npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@icedigital/aquabase

v0.7.0

Published

Ultra-fast encrypted real-time database client. Offline-first, zero codegen, cloud sync.

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 aquabase

Quick 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

  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

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

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

// 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", { isEqualTo: "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: When a field is auto-registered, the SDK logs a warning: [Aquabase] Index auto-created: "field" on "collection". Existing documents need a re-write (put()) to become queryable by the new field.

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

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

// From now on, put() auto-generates tags for 'date' and 'type'
await attendances.put("a1", {
  date: "2026-03-18",
  studentId: "stu_001",
  type: "present",
  notes: "On time",
});

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

Range Queries (ordered fields)

For fields where you need >, <, or BETWEEN queries (e.g., dates, scores, prices), use range operators. The SDK automatically selects the BTreeMap path instead of the equality HashMap.

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

// Register 'score' as a range-indexed field on first use
const results = await scores
  .where("schoolId", { isEqualTo: "school_01" })   // equality filter
  .where("score", { isGreaterThanOrEqual: 80 })    // range filter
  .where("score", { isLessThanOrEqual: 100 })
  .get();

// Range put: put() auto-generates the correct index entries
await scores.put("sc1", { schoolId: "school_01", score: 95 });

Available range operators:

| Operator | Example | |---|---| | isGreaterThan | { isGreaterThan: 80 } | | isGreaterThanOrEqual | { isGreaterThanOrEqual: 80 } | | isLessThan | { isLessThan: 100 } | | isLessThanOrEqual | { isLessThanOrEqual: 100 } | | isBetween | { isBetween: [80, 100] } |

Works with dates (ISO 8601), numbers, and strings. Dates and strings are sorted lexicographically; numbers use IEEE 754 big-endian encoding, so -5 < 0 < 100 is always correct.

Note: A field is either eq or range — not both. Calling .where('score', { isGreaterThan: ... }) upgrades the field from eq to range automatically. Existing docs require a re-write to become range-queryable.

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.

Direct Key Access

For raw key-value access without collection scoping:

// Write + read (raw keys)
app.putObject("users/u1", { name: "Ana", age: 25 });
const user = app.getObject("users/u1");

// Server fallback
const data = await app.getOrFetch("users/u1");

// List keys
const keys = await app.listKeys("users/");

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) | | storage | StorageAdapter | auto-detected | Custom cache adapter, false to disable | | l1Size | number | 1000 | L1 SIEVE cache size |

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 | | putObject(key, obj) | Write JS object locally + sync | | getObject(key) | Read + deserialize from local cache | | getOrFetch(key) | Local cache → server fallback | | delete(key) | Delete locally + sync | | bulkSet(entries) | Batch write | | bulkGet(keys) | Batch read | | bulkDelete(keys) | Batch delete | | isConnected | Server connection status | | pendingOps | Queued offline ops count | | close() | Flush + close connection |

Collection

| Method | Description | | ------------------- | ------------------------------------------------- | | put(id, obj) | Write document (auto-tags for indexed fields) | | get(id) | Read document (returns Promise) | | getOrFetch(id) | Cache → server fallback (cache-only if local) | | delete(id) | Delete document (cache-only if local) | | bulkSet(docs) | Batch write | | bulkGet(ids) | Batch read | | bulkDelete(ids) | Batch delete | | where(f, val) | Query Builder → .get() or .watch() | | watch(id, cb) | Real-time doc watch. Returns unsubscribe | | dispose() | Releases sync subscription | | isLocal | Whether the collection is local-only | | isEncrypted | Whether the collection is encrypted |

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() | Flush + disconnect |

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