{"_id":"@byearlybird/db","_rev":"2-147d8cb562366e4f3ec2f7aafbe80c59","name":"@byearlybird/db","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@byearlybird/db","version":"0.1.0","author":"Early Bird","license":"MIT","_id":"@byearlybird/db@0.1.0","maintainers":[{"name":"nickmurphy","email":"nickrmurphy@icloud.com"}],"homepage":"https://github.com/byearlybird/sdk#readme","bugs":{"url":"https://github.com/byearlybird/sdk/issues"},"dist":{"shasum":"56788182bf96e7ce61a05e6392990e6bd2bacead","tarball":"https://registry.npmjs.org/@byearlybird/db/-/db-0.1.0.tgz","fileCount":12,"integrity":"sha512-EUc64YX/GZYb28F6Ywrd29GiGVicdXQ3i37Eh8XaVYSCoiVD5FwjF35r3uhK4MqN7WrBYfezQVpKMWEWz3dWMQ==","signatures":[{"sig":"MEYCIQD1w4Fxcq2OYjw++FS6XZ/1KTI9gC8M8SRnB8HRuDPC8wIhALoaJg8L+CYZQjUFgrISVlrm/dN+kEu2BXsyTAU5ImSk","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":47762},"type":"module","exports":{".":"./dist/index.mjs","./opfs":"./dist/opfs.mjs","./capacitor":"./dist/capacitor.mjs","./package.json":"./package.json"},"scripts":{"dev":"vp pack --watch","test":"vp test","build":"vp pack","check":"vp check"},"_npmUser":{"name":"nickmurphy","email":"nickrmurphy@icloud.com"},"repository":{"url":"git+https://github.com/byearlybird/sdk.git","type":"git","directory":"packages/db"},"description":"A typed, reactive JSON document database built on SQLite.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"npm:@voidzero-dev/vite-plus-core@0.2.6","vite-plus":"0.2.6","typescript":"^7.0.2","@capacitor/core":"^8.4.2","@sqlite.org/sqlite-wasm":"3.53.0-build1","@capacitor-community/sqlite":"^8.1.0"},"peerDependencies":{"@capacitor/core":"^8.0.0","@sqlite.org/sqlite-wasm":"3.53.0-build1","@capacitor-community/sqlite":"^8.1.0"},"peerDependenciesMeta":{"@capacitor/core":{"optional":true},"@sqlite.org/sqlite-wasm":{"optional":true},"@capacitor-community/sqlite":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/db_0.1.0_1785899012317_0.8370644461230776","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@byearlybird/db","version":"0.2.0","description":"A typed, reactive JSON document database built on SQLite.","homepage":"https://github.com/byearlybird/sdk#readme","bugs":{"url":"https://github.com/byearlybird/sdk/issues"},"license":"MIT","author":{"name":"Early Bird"},"repository":{"type":"git","url":"git+https://github.com/byearlybird/sdk.git","directory":"packages/db"},"type":"module","sideEffects":false,"exports":{".":"./dist/index.mjs","./capacitor":"./dist/capacitor.mjs","./opfs":"./dist/opfs.mjs","./package.json":"./package.json"},"publishConfig":{"access":"public"},"dependencies":{"@byearlybird/sync":"0.2.0"},"devDependencies":{"@capacitor-community/sqlite":"^8.1.0","@capacitor/core":"^8.4.2","@sqlite.org/sqlite-wasm":"3.53.0-build1","typescript":"^7.0.2","vite":"npm:@voidzero-dev/vite-plus-core@0.2.6","vite-plus":"0.2.6"},"peerDependencies":{"@capacitor-community/sqlite":"^8.1.0","@capacitor/core":"^8.0.0","@sqlite.org/sqlite-wasm":"3.53.0-build1"},"peerDependenciesMeta":{"@capacitor-community/sqlite":{"optional":true},"@capacitor/core":{"optional":true},"@sqlite.org/sqlite-wasm":{"optional":true}},"scripts":{"build":"vp pack","dev":"vp pack --watch","test":"vp test","check":"vp check"},"_id":"@byearlybird/db@0.2.0","_integrity":"sha512-hRe/slCMxb+nkOv1WdXffauzBo0sCu1hXPsrcgpgT5eY5w8SR4+7QxOovuAZG9jcQvDG/eWmVPr+Kd7B8KGAvQ==","_resolved":"/private/var/folders/4h/bb9c_tbs151_51ycndd342740000gn/T/5c7f448872a2c98a4ccfb9210d1a96e0/byearlybird-db-0.2.0.tgz","_from":"file:byearlybird-db-0.2.0.tgz","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-hRe/slCMxb+nkOv1WdXffauzBo0sCu1hXPsrcgpgT5eY5w8SR4+7QxOovuAZG9jcQvDG/eWmVPr+Kd7B8KGAvQ==","shasum":"85785dc956546a7f06ce9d7a62a9c6981ff39edb","tarball":"https://registry.npmjs.org/@byearlybird/db/-/db-0.2.0.tgz","fileCount":13,"unpackedSize":72538,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDNBREvLn/gu20Jf5Zib2plOWaBl5M34jW3RB2fxxkMuQIhAMXdkPRL9YLmJm1dKcrRjOMxAWSiMyjpWQbg+6F55fl/"}]},"_npmUser":{"name":"nickmurphy","email":"nickrmurphy@icloud.com"},"directories":{},"maintainers":[{"name":"nickmurphy","email":"nickrmurphy@icloud.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/db_0.2.0_1786495758404_0.7722500286457143"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T03:03:32.117Z","modified":"2026-08-12T00:49:18.772Z","0.1.0":"2026-08-05T03:03:32.468Z","0.2.0":"2026-08-12T00:49:18.593Z"},"bugs":{"url":"https://github.com/byearlybird/sdk/issues"},"author":{"name":"Early Bird"},"license":"MIT","homepage":"https://github.com/byearlybird/sdk#readme","repository":{"type":"git","url":"git+https://github.com/byearlybird/sdk.git","directory":"packages/db"},"description":"A typed, reactive JSON document database built on SQLite.","maintainers":[{"name":"nickmurphy","email":"nickrmurphy@icloud.com"}],"readme":"# DB by Early Bird\n\nA typed, reactive JSON document database built on SQLite.\n\nDescribe your collections as TypeScript types and get inferred reads, writes, filtering, and\nordering. Queries rerun when the data behind them changes, and every write also produces a sync\nchange you can ship over whatever transport you like.\n\n> [!NOTE]\n> **Status: Beta.** The public API is mostly settled, but I'm not calling it done yet. Breaking\n> changes are still possible before 1.0, and I'll call them out in the changelog.\n\n## Install\n\n```sh\npnpm add @byearlybird/db\n```\n\nThe database talks to SQLite through a storage adapter, and each adapter brings its own peer\ndependency. Just install the one that matches where your app runs:\n\n| Environment    | Import                      | Also install                                     |\n| -------------- | --------------------------- | ------------------------------------------------ |\n| Browser (OPFS) | `@byearlybird/db/opfs`      | `@sqlite.org/sqlite-wasm`                        |\n| Capacitor      | `@byearlybird/db/capacitor` | `@capacitor-community/sqlite`, `@capacitor/core` |\n\n## Create a database\n\nA schema is a plain TypeScript type mapping collection names to their document shapes. That's all\nyou need. `@byearlybird/schema` is a nice fit for validating input, but this package doesn't depend\non it.\n\n```ts\nimport { createDatabase } from \"@byearlybird/db\";\nimport { opfsStorageAdapter } from \"@byearlybird/db/opfs\";\n\ntype AppSchema = {\n  entries: {\n    content: string;\n    createdAt: string;\n  };\n};\n\nconst database = createDatabase<AppSchema>({\n  name: \"app\",\n  storage: opfsStorageAdapter,\n});\n```\n\nOn Capacitor, swap the adapter for `createCapacitorStorageAdapter()` from\n`@byearlybird/db/capacitor`.\n\n## Write\n\nDocuments are addressed by a collection name and an ID you choose:\n\n```ts\nawait database.insert(\"entries\", \"entry-1\", {\n  content: \"First entry\",\n  createdAt: new Date().toISOString(),\n});\n\nawait database.patch(\"entries\", \"entry-1\", { content: \"Updated entry\" });\nawait database.delete(\"entries\", \"entry-1\");\n```\n\n`patch` merges the fields you pass; `patch` and `delete` return `false` when the document does not\nexist.\n\n### Batches\n\nUse `batch` to apply a few mutations atomically. The callback records mutations in invocation order\nand has to be synchronous:\n\n```ts\nawait database.batch((mutation) => {\n  mutation.insert(\"entries\", \"entry-1\", {\n    content: \"First entry\",\n    createdAt: new Date().toISOString(),\n  });\n  mutation.patch(\"entries\", \"entry-2\", { content: \"Updated entry\" });\n  mutation.delete(\"entries\", \"entry-3\");\n});\n```\n\nIf any mutation fails, the whole batch rolls back. Change listeners only hear about it after the\ntransaction commits.\n\n## Read\n\n`get` returns one document's data, `getAll` returns every entry in a collection, and `query` filters\nand orders. Reads that return entries give you `{ id, data }` pairs:\n\n```ts\nconst entry = await database.get(\"entries\", \"entry-1\");\nconst all = await database.getAll(\"entries\");\n\nconst recent = await database.query(\"entries\", (query) => ({\n  where: query.gte(\"createdAt\", \"2026-01-01\"),\n  orderBy: [query.desc(\"createdAt\")],\n  limit: 20,\n}));\n\nfor (const { id, data } of recent) {\n  console.log(id, data.content);\n}\n```\n\nThe builder is typed against the document shape, so field names and value types get checked, and\n`id` is always there as a field.\n\n| Builder                               | Matches                                   |\n| ------------------------------------- | ----------------------------------------- |\n| `eq(field, value)`                    | Exact scalar equality                     |\n| `gt` / `gte` / `lt` / `lte`           | Ordered comparison on numbers and strings |\n| `in(field, values)` / `notIn`         | Membership in a fixed set                 |\n| `includes(field, value)` / `excludes` | An element inside a scalar array field    |\n| `and(...)` / `or(...)`                | Grouped predicates                        |\n| `asc(field)` / `desc(field)`          | Ordering, passed in `orderBy`             |\n\n`limit` and `offset` paginate.\n\n### Live queries\n\n`createQuery` runs a read right away, then reruns it whenever one of the collections it touched\nchanges. It tracks those collections for you, so there's nothing to declare:\n\n```ts\nimport { createQuery } from \"@byearlybird/db\";\n\nconst query = createQuery(database, (readonlyDatabase) =>\n  readonlyDatabase.query(\"entries\", (entry) => ({ orderBy: [entry.desc(\"createdAt\")] })),\n);\n\nconst unsubscribe = query.subscribe(() => {\n  const snapshot = query.getSnapshot();\n  if (snapshot.status === \"success\") render(snapshot.value);\n});\n```\n\n`getSnapshot` gives you a `pending`, `success`, or `error` snapshot. In React, I'd reach for\n[`@byearlybird/db-react`](https://github.com/byearlybird/sdk/tree/main/packages/db-react) rather than\nwiring this up by hand.\n\nIf you want lower-level notifications, `database.onChange` fires with the collection, ID, and\noperation for each committed mutation, and returns an unsubscribe function.\n\n## Sync\n\nEvery committed mutation produces a coalesced sync change. Here's the gist: changes carry complete\nentity snapshots or permanent tombstones, and they use Lamport versions so conflict resolution comes\nout the same everywhere. Your app can trade changes over any transport by reading, applying, and\nacknowledging batches:\n\n```ts\nconst changes = await firstDatabase.getPendingChanges(100);\n\nawait secondDatabase.applyRemoteChanges(changes);\nawait firstDatabase.acknowledgeChanges(changes.map(({ changeId }) => changeId));\n```\n\nApplying changes is repeat-safe, and it still observes remote clocks even when the local entity\nwins. Acknowledgments match the current change ID, so acknowledging an in-flight change can't clear\na newer local mutation for the same entity.\n\nThe shared change, clock, and transport types come from\n[`@byearlybird/sync`](../sync). DB re-exports the existing types, so imports from `@byearlybird/db`\nkeep working. DB itself uses plaintext changes and does not require or enable encryption. An app can\nchoose to encrypt changes at its transport boundary with `@byearlybird/sync/crypto`.\n\n### The synchronizer\n\n`createSynchronizer` handles the paginated pulling, checkpoint persistence, and outbox pushing for\nyou. You bring the transport:\n\n```ts\nimport { createSynchronizer } from \"@byearlybird/db\";\nimport type { SyncTransport } from \"@byearlybird/sync\";\n\nconst transport: SyncTransport = {\n  pull: async ({ cursor, limit }) => {\n    // Return changes after the opaque cursor and a durable next cursor.\n    return relay.pull({ cursor, limit });\n  },\n  push: async ({ changes }) => {\n    // Resolve only after every change has been durably accepted.\n    await relay.push(changes);\n  },\n};\n\nconst synchronizer = createSynchronizer(database, { transport });\nawait synchronizer.sync();\n```\n\nA few details worth knowing:\n\n- Pull pages commit atomically with their opaque checkpoints.\n- Pushes are repeat-safe when the server treats the same `changeId` and version as a retry, so a lost\n  response leaves the local outbox intact for another attempt.\n- Syncing pulls before it pushes, and concurrent calls on one synchronizer share the same run.\n- So, stick to one sync upstream for a database's lifetime.\n\n> [!IMPORTANT]\n> The synchronizer doesn't schedule, retry, time out, cancel, or back off. Those policies are on you\n> to provide around `sync()`, and transport pushes need to be idempotent by `changeId`.\n\n`apps/demo-server` in this repository is a pretty minimal relay that implements both transport\nmethods over HTTP, if you want something to copy from.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}