@icedigital/aquabase
v0.7.1
Published
Ultra-fast encrypted real-time database client. Offline-first, zero codegen, cloud sync.
Maintainers
Readme
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
npm install aquabaseQuick Start
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: "[email protected]" });
// Auth
const { uid, token } = await app.auth.register("[email protected]", "secret123");
// Storage
await app.storage.upload("avatars", "photo.jpg", file);
// Logs
app.logs.connect();
app.logs.info("db", "User created account");How It Works
- All reads/writes go to the local cache — instant, no network latency
- When online, changes sync to the server in background (auto-batched)
- When offline, failed ops queue to disk automatically
- On reconnect, the queue flushes to the server — no data loss
Collections
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.
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
// 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.
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:
// Per-collection: only sensitive collections are encrypted
const session = app.collection("_session", { local: true, encrypted: true });
await session.put("current", { jwt: token, email: "[email protected]" });
// 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:
// 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
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.
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.
// 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. Callput()again on those documents to make them appear in range queries.
Custom Server URL
const app = new Aquabase({
projectId: "your_project_id",
apiKey: "aq_pub_...",
url: "http://localhost:3280",
});Auth
// Email/password
const { uid, token } = await app.auth.register("[email protected]", "secret123");
const result = await app.auth.login("[email protected]", "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
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.
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
