npm package: @icedigital/aquabase
Description: Ultra-fast encrypted real-time database client. Offline-first, zero codegen, cloud sync.
Latest version: 0.7.1

# Aquabase

Ultra-fast offline-first database with automatic cloud sync, user auth, and file storage. Zero codegen, pure TypeScript.

Works in **Node.js**, **Bun**, **Deno**, and **Browser**.

## Install

```bash
npm install aquabase
```

## Quick Start

```typescript
import { Aquabase } from "aquabase";

const app = new Aquabase({
  projectId: "your_project_id",
  apiKey: "aq_pub_...",
  // encrypted: true,          // optional: encrypt ALL collections
  // jwt: "user-jwt-token",    // optional: for auth/self rules
});

app.connect();

// Database
const users = app.collection("users");
await users.put("u1", { name: "Ana", age: 25 });
const user = await users.get("u1");

// Encrypted collection (AES-256-GCM)
const session = app.collection("_session", { local: true, encrypted: true });
await session.put("current", { jwt: token, email: "user@test.com" });

// Auth
const { uid, token } = await app.auth.register("user@example.com", "secret123");

// Storage
await app.storage.upload("avatars", "photo.jpg", file);

// Logs
app.logs.connect();
app.logs.info("db", "User created account");
```

## How It Works

1. **All reads/writes go to the local cache** — instant, no network latency
2. **When online**, changes sync to the server in background (auto-batched)
3. **When offline**, failed ops queue to disk automatically
4. **On reconnect**, the queue flushes to the server — no data loss

## Collections

```typescript
const users = app.collection("users");

// Document CRUD
await users.doc("u1").set({ name: "Ana", age: 25 });
const user = await users.doc("u1").get();
users.doc("u1").delete();

// Auto-generated ID
const id = await users.add({ name: "Carlos", age: 30 });
console.log(id); // e.g. 'aB3xK9mQ7pR2wT4vN1'

// Bulk operations (single WS frame per chunk)
await users.bulkSet(
  new Map([
    ["u1", { name: "Ana" }],
    ["u2", { name: "Luis" }],
  ]),
);

const results = await users.bulkGet(["u1", "u2"]);
await users.bulkDelete(["u1", "u2"]);
```

### Real-Time

Watch a document or a query for changes — fires immediately with current data, then on every update from any device.

```typescript
const users = app.collection("users");

// Watch a single document
const unsub = users.watch("u1", (user) => {
  if (user) renderUser(user);
});

// Watch a query
const unsub2 = users.where("status", { eq: "active" }).watch((results) => {
  for (const [id, user] of results) {
    console.log(id, user);
  }
});

// Stop watching
unsub();
unsub2();
```

### Server Fallback

```typescript
// Read from cache, fallback to server if not cached
const data = await users.getOrFetch("u1");
```

## Local Collections

Same API as any collection, but data stays on the device — no server sync.

```typescript
const students = app.collection("_students", { local: true });

// Same API as any collection
await students.put("s1", { name: "Ana", age: 12, grade: "6A" });
const student = await students.get("s1");
students.delete("s1");

await students.bulkSet(
  new Map([
    ["s1", { name: "Ana", age: 12 }],
    ["s2", { name: "Luis", age: 13 }],
  ]),
);
```

> A collection name cannot be both local and remote simultaneously.

## Encryption

AES-256-GCM encryption, two modes:

```typescript
// Per-collection: only sensitive collections are encrypted
const session = app.collection("_session", { local: true, encrypted: true });
await session.put("current", { jwt: token, email: "user@test.com" });

// Global: ALL collections are encrypted
const app = new Aquabase({
  projectId: "your_project_id",
  apiKey: "aq_pub_...",
  encrypted: true,
});
```

When `encrypted: true` is set globally, every collection is encrypted automatically — no need to set `encrypted` on each one.

## Date Handling

`DateTime`/`Date` objects are automatically serialized as **milliseconds since epoch (UTC)**. On read, timestamps are returned as `number` — convert in your app:

```typescript
// Write
await users.put("u1", { name: "Ana", createdAt: new Date() });

// Read
const user = await users.get<{ name: string; createdAt: number }>("u1");
const date = new Date(user.createdAt);
```

This format is cross-platform compatible with the Flutter SDK.

## Indexes

Indexes are **automatic**. The first time you use `.where('field', ...)`, the SDK registers that field as indexed and syncs it to the server. From that point on, every `put()` auto-generates tags for queried fields. No manual configuration needed.

> **Note:** Existing documents need a re-write (`put()`) to become queryable by a newly indexed field.

### Equality

```typescript
const attendances = app.collection("attendances");

// .where() auto-registers 'date' and 'type' as indexed fields
const results = await attendances
  .where("date", { eq: "2026-03-18" })
  .where("type", { eq: "present" })
  .get();

// Real-time query
const unsub = attendances
  .where("date", { eq: "2026-03-18" })
  .watch((results) => {
    for (const [id, data] of results) {
      console.log(id, data);
    }
  });
unsub();
```

### Range Queries

Use `from`/`to` for **inclusive** ranges (most common). The SDK automatically selects the BTreeMap index.

```typescript
const scores = app.collection("scores");

// Scores between 80 and 100
const high = await scores
  .where("score", { from: 80, to: 100 })
  .get();

// March attendances for a student
const march = await attendances
  .where("date", { from: "2026-03-01", to: "2026-03-31" })
  .where("studentId", { eq: "stu_001" })
  .get();

// Exclusive comparisons
const above80 = await scores
  .where("score", { gt: 80 })
  .get();
```

**All available operators:**

| Operator | Type | Description |
|----------|------|-------------|
| `eq`   | any | Exact equality |
| `from` | num / string | Range start (≥ inclusive) |
| `to`   | num / string | Range end (≤ inclusive) |
| `gt`   | num / string | Greater than (> exclusive) |
| `lt`   | num / string | Less than (< exclusive) |

Works with dates (ISO 8601), numbers, and strings. Numbers use IEEE 754 big-endian encoding, so `-5 < 0 < 100` is always correct.

### Ordering

Use `orderBy()` to sort results client-side. Works with any numeric or string field.

```typescript
// Scores highest first
const top = await scores
  .where("classId", { eq: "math_101" })
  .orderBy("score", { descending: true })
  .get();

// Attendances sorted by date
const sorted = await attendances
  .where("studentId", { eq: "stu_001" })
  .orderBy("date")
  .get();
```

> **Historical data:** Existing documents written before a field was registered as `range`-indexed are not automatically re-indexed. Call `put()` again on those documents to make them appear in range queries.



## Custom Server URL

```typescript
const app = new Aquabase({
  projectId: "your_project_id",
  apiKey: "aq_pub_...",
  url: "http://localhost:3280",
});
```

## Auth

```typescript
// Email/password
const { uid, token } = await app.auth.register("user@example.com", "secret123");
const result = await app.auth.login("user@example.com", "secret123");

// OAuth (Google, GitHub)
const google = await app.auth.signInWithOAuth("google", googleIdToken);
const github = await app.auth.signInWithOAuth("github", githubCode);

// Connect with user identity
const userApp = new Aquabase({
  projectId: "...",
  apiKey: "aq_pub_...",
  jwt: result.token,
});
userApp.connect();
```

## Storage

```typescript
await app.storage.createBucket({ name: "avatars", public: true });
await app.storage.upload("avatars", "photo.jpg", file);

const blob = await app.storage.download("avatars", "photo.jpg");
const files = await app.storage.listFiles("avatars");
await app.storage.deleteFile("avatars", "photo.jpg");
```

## Logs

Structured log collection with auto-batching.

```typescript
app.logs.connect();

app.logs.info("db", "User created account");
app.logs.warn("auth", "Invalid password attempt", "192.168.1.1");
app.logs.error("ws", "Connection timeout");

const records = await app.logs.query({
  since: Date.now() * 1_000_000 - 3600e9,
  minLevel: LogLevel.Warn,
  channel: "auth",
  limit: 100,
});

app.logs.close();
```

## API Reference

### Constructor Options

| Option      | Type             | Default        | Description                              |
| ----------- | ---------------- | -------------- | ---------------------------------------- |
| `projectId`   | `string`         | —              | Project ID for cache identity            |
| `apiKey`      | `string`         | —              | API key for the server                   |
| `url`         | `string`         | Aquabase cloud | Server URL                               |
| `jwt`         | `string`         | —              | JWT for authenticated users              |
| `encrypted`   | `boolean`        | `false`        | Encrypt ALL collections (AES-256-GCM)    |

### Aquabase

| Method                         | Description                                   |
| ------------------------------ | --------------------------------------------- |
| `collection(name)`                   | Get a collection (synced)                     |
| `collection(name, {local: true})`     | Get a local-only collection                   |
| `collection(name, {encrypted: true})` | Get an encrypted collection (AES-256-GCM)     |
| `connect()`                    | Connect to server                             |
| `isConnected`                  | Server connection status                      |
| `pendingOps`                   | Queued offline ops count                      |
| `close()`                      | Close connection + release resources          |

### Collection

| Method              | Description                                       |
| ------------------- | ------------------------------------------------- |
| `doc(id)`           | Document reference → `.set()` `.get()` `.delete()` |
| `add(obj)`          | Write with auto-generated ID, returns the ID      |
| `bulkSet(docs)`     | Batch write                                       |
| `bulkGet(ids)`      | Batch read                                        |
| `bulkDelete(ids)`   | Batch delete                                      |
| `where(f, ...)`     | Query Builder → `.orderBy()` `.get()` `.watch()`  |
| `watch(id, cb)`     | Real-time doc watch. Returns unsubscribe          |

### Auth

| Method                                     | Description                                 |
| ------------------------------------------ | ------------------------------------------- |
| `app.auth.register(email, password)`       | Register user, returns JWT + uid            |
| `app.auth.login(email, password)`          | Login user, returns JWT + uid               |
| `app.auth.signInWithOAuth(provider, cred)` | OAuth login (Google/GitHub), auto-registers |
| `app.auth.refresh(token)`                  | Refresh an expired JWT                      |

### Storage

| Method                                          | Description               |
| ----------------------------------------------- | ------------------------- |
| `app.storage.listBuckets()`                     | List all buckets          |
| `app.storage.createBucket(opts)`                | Create a bucket           |
| `app.storage.deleteBucket(name)`                | Delete bucket + all files |
| `app.storage.upload(bucket, path, data, type?)` | Upload/overwrite a file   |
| `app.storage.download(bucket, path)`            | Download as Blob          |
| `app.storage.deleteFile(bucket, path)`          | Delete a file             |

### Logs

| Method                              | Description                                   |
| ----------------------------------- | --------------------------------------------- |
| `app.logs.connect()`                | Connect to server (auto-called on first push) |
| `app.logs.info(channel, msg, src)`  | Push info-level log                           |
| `app.logs.warn(channel, msg, src)`  | Push warn-level log                           |
| `app.logs.error(channel, msg, src)` | Push error-level log                          |
| `app.logs.flush()`                  | Flush buffer to server                        |
| `app.logs.query(opts?)`             | Query stored logs                             |
| `app.logs.close()`                  | Disconnect + release resources                |

## Architecture

| Component     | Implementation                                      |
| ------------- | --------------------------------------------------- |
| Serialization | MsgPack (msgpackr) — Date → ms epoch auto-conversion |
| L1 Cache      | SIEVE eviction (NSDI '24)                           |
| L2 Storage    | CompactCache — snapshot reads + append-only disk     |
| Cloud Sync    | WebSocket + OP_PIPELINE auto-batching               |
| Offline Queue | Persisted to disk, auto-flush on reconnect          |

## License

Proprietary