# nookdb — full documentation > Schema-first, reactive, local-first database for Electron desktop apps, built on a Rust core (redb 2.x). This file concatenates the complete documentation for AI assistants. Paste it into your tool of choice, or point your assistant at https://nookdb.pages.dev/llms-full.txt. # Quick start Source: https://nookdb.pages.dev/quick-start/ > Install nookdb and open your first database in under a minute. nookdb is a Rust-powered, schema-first, reactive local database for Electron desktop apps. This page gets you to a working database in three steps, then shows the two things you'll reach for next: the Electron bridge and typed errors. ## Install ```bash pnpm add nookdb ``` ```bash npm install nookdb ``` ```bash yarn add nookdb ``` ```bash bun add nookdb ``` nookdb ships precompiled native binaries for Win/macOS/Linux × x64/arm64. No build toolchain required. ## Define a schema ```ts export const schema = { users: s .collection({ id: s.id(), // UUID v7 email: s.string().email(), role: s.enum(['admin', 'user']).default('user'), createdAt: s.date().default(() => new Date()), }) .uniqueIndex('email') .index('role'), }; ``` ## Open, insert, query ```ts const db = await open('./app.db', { schema }); await db.users.insert({ email: 'ali@example.com', role: 'admin' }); const admins = await db.users.find({ role: 'admin' }); // ^? { id: string; email: string; role: 'admin' | 'user'; createdAt: Date }[] db.close(); ``` That's it. `db.users` is fully typed from the schema; the Rust core is the authoritative validator. ## …or wire it into Electron in 5 lines The whole point of nookdb is that your main process owns the database and your renderers consume it like a local API — no `ipcMain.handle` plumbing, no manual broadcast on writes. Install the bridge alongside the core: ```bash pnpm add nookdb @nookdb/electron ``` ```bash npm install nookdb @nookdb/electron ``` ```bash yarn add nookdb @nookdb/electron ``` ```bash bun add nookdb @nookdb/electron ``` ```ts title="main.ts" const host = await openHost('./app.db', { schema }); // for each window: hand it one end of a MessageChannel and keep the other. const { port1, port2 } = new MessageChannelMain(); host.connectPort(port1, { frameUrl: win.webContents.getURL(), origin: null, webContentsId: win.webContents.id }); win.webContents.postMessage('nook:port', null, [port2]); ``` ```ts title="preload.ts" exposeNookBridge(); // forwards a MessagePort to window.nookdb ``` ```ts title="renderer.ts" const db = await connectNook({ schema }); const admins = await db.users.find({ role: 'admin' }); // same API, typed ``` The wire protocol is structured-clone over `MessageChannel`. A schema-hash handshake rejects mismatched schemas on connect; per-renderer subscriptions clean up on disconnect. See the [Electron bridge guide](/guides/electron-bridge/) for the full setup including authorizers and `live()` subscriptions. ## Errors are typed nookdb errors aren't string-matched — they're a discriminated `NookError` hierarchy you can `instanceof`-narrow on. The Rust core is the authoritative validator; failures cross the FFI boundary with a `[kind] message` prefix and the TS layer re-hydrates them into the matching subclass: ```ts try { await db.users.insert({ email: 'not-an-email', role: 'admin' }); } catch (err) { if (err instanceof NookSchemaError) { // err.message → "[schema] users.email failed string.email" return showFormError(err.message); } if (err instanceof NookConflictError) { // unique-index violation, e.g. duplicate email return showFormError('email already in use'); } throw err; } ``` The `[kind]` prefix is part of the stable public API — branching on `instanceof` is the recommended path. Full catalogue: [`Errors` reference](/reference/errors/). ## Next steps - Browse the [Schema DSL guide](/guides/schema-dsl/) to see every builder. - Try [Reactive `live()`](/guides/reactive-live/) for UI that stays in sync with the database. - Read the [Architecture overview](/architecture/overview/) for how the Rust core, NAPI binding, and Electron bridge fit together. --- # Schema DSL Source: https://nookdb.pages.dev/guides/schema-dsl/ > The s.* schema builder — every field, constraint, and index shipped in v0.x. The `s.*` namespace is nookdb's schema DSL. Every type, validator, and index in your app starts here. ## Shipped builders ```ts const schema = { users: s.collection({ id: s.id(), // UUID v7, monotonic email: s.string().email(), // RFC 5322 validation name: s.string().min(1).max(100), age: s.number().int().min(0).optional(), // optional → undefined permitted role: s.enum(['admin', 'user']).default('user'), tags: s.array(s.string()), createdAt: s.date().default(() => new Date()), }), }; ``` ### Available builders - `s.id()` — UUID v7 string. Auto-generated on insert if you don't supply one. - `s.string()` — UTF-8 string. Chain `.min(n)`, `.max(n)`, `.email()`, `.regex(/…/)`, `.nullable()`, `.optional()`, `.default(value | () => value)`. - `s.number()` — JS number. Chain `.int()`, `.min(n)`, `.max(n)`, `.nullable()`, `.optional()`, `.default(...)`. - `s.boolean()` — Chain `.nullable()`, `.optional()`, `.default(true | false)`. - `s.date()` — JS `Date`. Chain `.nullable()`, `.optional()`, `.default(...)`. - `s.enum([...] as const)` — string literal union. The literal types flow through to `$type`, not just `string`. Chain `.default(...)`. - `s.array(itemBuilder)` — homogeneous array of any primitive builder (string / number / boolean / date / enum / nested array). - `s.collection({...})` — wraps a field record into a collection. Chain `.index('field')`, `.index(['a', 'b'])`, `.uniqueIndex('field')`. ## Indexes ```ts const schema = { posts: s .collection({ /* fields */ }) .index('authorId') // single-field .index(['authorId', 'publishedAt']) // composite (prefix-usable) .uniqueIndex('slug'), // unique + index }; ``` Indexes are declared on the schema — there is no runtime `createIndex`. Adding or removing an index triggers a schema-version bump that the [migration ledger](/guides/migrations/) tracks. ## Type inference ```ts type User = typeof schema.users.$type; // { id: string; email: string; name: string; age?: number | undefined; role: 'admin' | 'user'; ... } ``` Use `$type` in app code to keep handwritten types and runtime validation in lockstep. The Rust core re-validates every write against the same descriptor; if the TS type says `role: 'admin' | 'user'` then Rust will reject `role: 'mod'` even when TS lies. ## Not yet shipped These appear in early PRDs and design discussions but are **not callable in v0.x** — listed here so you don't reach for them and find a `TypeError`: - **`s.object({...})`** — nested struct fields. Currently `s.collection` is the only structured-record builder; nesting is post-1.0 work (`# v2` in `src/schema/s.ts`). - **`s.ref(() => schema.users)`** — typed foreign-key references. The same query-time joining can be done by storing the parent id as `s.string()` and querying both collections; the typed-ref ergonomics are post-1.0. - **`{ sparse: true }` index option** — null-skipping indexes. Currently all indexes include null values; declare the field `.nullable()` and filter `== null` rows out of your queries. When these land they'll be in [the changelog](https://github.com/nookwright/nookdb/blob/main/CHANGELOG.md). The shipped surface above is stable across v0.x. --- # Queries Source: https://nookdb.pages.dev/guides/queries/ > find, findOne, count, update, delete — the shipped query surface for v0.x. After [opening a database with a schema](/quick-start/), every collection exposes a typed query API. ## Shipped surface ```ts await db.users.find(); // all rows await db.users.find({ role: 'admin' }); // equality filter await db.users.findOne({ email: 'ali@example.com' }); // first or null await db.users.count({ role: 'admin' }); // number await db.users.delete({ role: 'user' }); // returns count removed await db.users.update( { id: '0193…' }, // filter (must match ≤ 1) { role: 'admin' }, // partial patch ); // returns count updated ``` Each method returns its result type inferred from the schema — `find()` returns `Doc[]`, `findOne()` returns `Doc | null`, the mutating methods return `number`. The filter is a plain `Record` of field-value pairs: every key is interpreted as **strict equality** against that field. Compound filters narrow with logical AND. ```ts await db.users.find({ role: 'admin', active: true }); // role='admin' AND active=true ``` Indexed fields (declared with `.index(...)` / `.uniqueIndex(...)` in the schema) are looked up via the secondary index. Non-indexed filters fall back to a full scan — fine for small collections, expensive at scale; index the fields you query. ## Reactive sibling Every read above has a live counterpart: ```ts const lq = db.users.live({ role: 'admin' }); const unsub = lq.subscribe((users) => render(users)); // later: unsub(); lq.dispose(); ``` See the [Reactive guide](/guides/reactive-live/) for the full subscription model (`subscribe`, `for await`, React's `useLive`). ## Transactions Multi-document writes commit atomically: ```ts await db.transaction(async (tx) => { await tx.users.insert({ email: 'ali@example.com', role: 'admin' }); await tx.posts.insert({ authorId: '0193…', title: 'Hi' }); }); // commit on return, rollback on throw ``` Inside the callback, `tx.users` is a transaction-scoped proxy with the same surface as `db.users` (`insert`, `find`, `findOne`, `count`, `delete`). Throw to roll back; return to commit. Nested `db.transaction(...)` calls are rejected. ## Not yet shipped The following are on the roadmap (post-v0.x) and **not callable today** — listed here so you don't reach for them and find a TypeError: - **Comparison operators** (`$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$in`, `$nin`, `$exists`, `$regex`). For now: pull the rows with an equality filter and narrow in JS, or add an index and use multiple `count` / `findOne` calls. - **Fluent builder** (`.where(...).orderBy(...).limit(...)`). For now: filter via the equality-object form and sort/slice the array client-side. - **Aggregations** (`groupBy`, `sum`, `avg`). For now: stream rows with `live({...})` and aggregate in JS, or — if your dataset outgrows that — this is a sign you want SQLite, not NookDB. When these land they'll be in [the changelog](https://github.com/nookwright/nookdb/blob/main/CHANGELOG.md). The shipped surface above is stable across v0.x. --- # Reactive: live() Source: https://nookdb.pages.dev/guides/reactive-live/ > Snapshot semantics, subscribe vs AsyncIterator, useLive() for React. Every collection's `find`-style query has a `.live(...)` sibling that emits a new snapshot on every committed write that touches a matching document. ## Subscribe pattern ```ts const lq = db.users.live({ role: 'admin' }); const off = lq.subscribe((admins) => { console.log(`current admins: ${admins.length}`); }); // ... later off(); // unsubscribe one callback lq.dispose(); // tear down the subscription entirely ``` ## AsyncIterator pattern ```ts for await (const admins of db.users.live({ role: 'admin' })) { render(admins); } ``` The iterator yields the same snapshots the `subscribe()` callback would receive. Exiting the loop disposes the subscription. ## Sync access ```ts const lq = db.users.live({ role: 'admin' }); lq.value; // current snapshot (undefined until first emission) ``` ## React: `useLive` ```tsx function AdminList({ db }) { const admins = useLive(() => db.users.live({ role: 'admin' }), [db]); return ( ); } ``` The hook handles subscription + cleanup; the result is the current snapshot. ## Coalescing Rapid writes produce a single emission per "burst" — the engine coalesces and the subscriber sees only the latest snapshot. This protects backpressured renderers from update storms. ## What live() does NOT do `live()` re-runs the matching `find` query inside the engine after every committed write. It is **not** a CRDT and does **not** sync across machines. For multi-process Electron apps see the [Electron bridge guide](/guides/electron-bridge/). --- # Migrations Source: https://nookdb.pages.dev/guides/migrations/ > The version ledger, migrate status / up, and what's deferred. nookdb's migration runner is a small version-tracking ledger. It records which integer versions have been applied; the full migration DSL (up/down/backfill) is deferred to a later release. ## Programmatic API ```ts const db = await open('./app.db', { schema }); const status = await db.migrateStatus(); // { currentVersion: 0, appliedCount: 0 } await db.migrateRun([1, 2, 3]); const applied = await db.migrateListApplied(); // [1, 2, 3] ``` `migrateRun` is idempotent — applying a version that is already recorded is a no-op. Versions are persisted in a dedicated `_meta` collection in the same DB file. ## CLI ```bash nookdb migrate status ./app.db # current_version: 0 # applied: [] nookdb migrate up ./app.db --versions 1,2,3 # Migration ledger updated: 3 version(s) recorded. ``` ## Concurrency `migrateRun` reads-merges-writes the ledger inside a single redb write transaction (hardened in M5a). Two concurrent `migrateRun` calls cannot lost-update each other. ## What's deferred The DSL form (`defineMigration({ version, up, down })` with backfill helpers) is not in this release. The Runner today is the version-tracker the full DSL will attach to without crate changes. --- # CLI Source: https://nookdb.pages.dev/guides/cli/ > nookdb command reference — backup, restore, migrate, inspect. `@nookdb/cli` ships a small `nookdb` binary for backup, restore, migration ledger management, and database inspection. ## Install ```bash pnpm add -D @nookdb/cli # Or globally: pnpm add -g @nookdb/cli ``` The CLI runs on Node 20+; no platform-specific build step. ## Commands ### `nookdb backup ` Create a portable `.nbkp` snapshot of `` at ``. The write is atomic (writes to `.tmp` then renames). ```bash nookdb backup ./app.db ./snapshots/2026-05-20.nbkp # Backup written: 1234 entries, 5678 bytes → ./snapshots/2026-05-20.nbkp ``` ### `nookdb restore ` Restore `` from a `.nbkp` file. By default, the target DB must be empty. ```bash nookdb restore ./snapshots/2026-05-20.nbkp ./app.db --allow-overwrite # Restore complete: 1234 entries (5678 bytes read) → ./app.db ``` Options: - `--allow-overwrite` — allow restoring into a non-empty DB (destructive). - `--skip-schema-check` — skip the backup↔DB schema_hash validation. - `--create` — create `` if it does not exist. ### `nookdb migrate status ` ```bash nookdb migrate status ./app.db # current_version: 3 # applied: [1, 2, 3] ``` ### `nookdb migrate up --versions n,m,...` ```bash nookdb migrate up ./app.db --versions 1,2,3 # Migration ledger updated: 3 version(s) recorded. ``` Forward-only. Already-applied versions are silently skipped. ### `nookdb inspect ` Prints DB path, file size, migration ledger, and per-collection entry counts. ```bash nookdb inspect ./app.db # Path: ./app.db # File size: 12.4 MiB (13,033,472 bytes) # Migration: # current_version: 3 # applied: [1, 2, 3] # Collections: # users 1240 entries # posts 312 entries ``` ## Exit codes - `0` — success - `1` — runtime failure (with `error: [kind] message` on stderr) - `2` — usage error (missing arg, unknown command) --- # Electron bridge Source: https://nookdb.pages.dev/guides/electron-bridge/ > Main + preload + renderer setup, schema-hash handshake, Authorizer. The `@nookdb/electron` package lets Electron renderers use the same `db.users.find(...)` / `db.users.live(...)` API as the main process — no IPC code, no manual serialization. ## Setup ### `main.ts` ```ts let host: Awaited>; app.whenReady().then(async () => { host = await openHost('./app.db', { schema }); const win = new BrowserWindow({ webPreferences: { preload: __dirname + '/preload.js', contextIsolation: true }, }); const { port1, port2 } = new MessageChannelMain(); host.connectPort(port1, { frameUrl: win.webContents.getURL(), origin: null, webContentsId: win.webContents.id, }); win.webContents.postMessage('nook:port', null, [port2]); win.loadFile('index.html'); }); ``` ### `preload.ts` ```ts exposeNookBridge(); ``` ### `renderer.ts` ```ts const db = await connectNook({ schema }); const admins = await db.users.find({ role: 'admin' }); for await (const list of db.users.live({ role: 'admin' })) render(list); ``` The renderer-side `db` is a typed proxy. Method calls forward to the main-process Host over the MessageChannel; live subscriptions push from main back to renderer. ## Schema-hash handshake When a renderer connects, it sends its schema hash. Main compares to its own; on mismatch, the connection is rejected with a `NookSchemaError`. This prevents silently divergent schemas from corrupting state. ## Authorizer Default: every renderer can call every op. To gate: ```ts const acl: Authorizer = { authorize(sender, op) { if (sender.frameUrl?.startsWith('app://')) return 'allow'; if (op.kind === 'find') return 'allow'; return 'deny'; }, }; const host = await openHost('./app.db', { schema, authorizer: acl }); ``` A `deny` decision throws `NookForbiddenError` in the renderer. ## What about live() across windows? Yes — open the same app in two windows and writes from one render live updates in the other. The Host fans out commit notifications to every connected renderer that has an active live subscription matching the changed document. See the `electron-notes` example for a runnable demo. --- # Backup / restore Source: https://nookdb.pages.dev/guides/backup-restore/ > Programmatic API, .nbkp format, atomic write, orchestrator pattern. nookdb ships first-class backup and restore. The format is a portable `.nbkp` v1 binary that does NOT depend on the underlying redb file format — backups remain restorable across nookdb upgrades. ## Method form ```ts const db = await open('./app.db', { schema }); const stats = await db.backup('./snap.nbkp'); // { entryCount: 1234, bytesWritten: 56789 } // Later, on a fresh DB or with --allow-overwrite: const newDb = await open('./restored.db', { schema }); await newDb.restore('./snap.nbkp'); ``` ## Options ```ts await db.restore('./snap.nbkp', { allowOverwrite: true, // default false: throws NookConflictError if DB not empty skipSchemaCheck: true, // default false: throws NookSchemaError on schema_hash mismatch }); ``` ## Orchestrator pattern For scheduled-backup orchestrators (point-in-time snapshots, corruption recovery tools, etc.), use the freestanding helpers: ```ts await backupToPath(db, './snap.nbkp'); await restoreFromPath(db, './snap.nbkp', { allowOverwrite: true }); ``` These are exactly equivalent to the method form — same NAPI call. The freestanding form exists so external packages can attach without depending on the `Database` instance shape. ## Atomic write `backup` writes to `.tmp`, fsyncs, and atomically renames. A backup interrupted mid-write never produces a half-formed `.nbkp` file. ## CLI ```bash nookdb backup ./app.db ./snap.nbkp nookdb restore ./snap.nbkp ./app.db --allow-overwrite ``` See the [CLI guide](/guides/cli/) for full flag reference. ## File format The `.nbkp` v1 format is documented in the [reference](/reference/nbkp-format/) — header, length-prefixed entries, sentinel, CRC32 footer. Inspectable by any reader that follows the spec. --- # Migrating from sehawq.db v5 Source: https://nookdb.pages.dev/guides/migrating-from-sehawq-v5/ > A 50-line manual migration script — no CLI, no auto-import. ## Why is there no automated importer? The earlier plan was to ship a `nookdb import --from sehawq-v5` CLI command. That plan was withdrawn during the M5a brainstorm (2026-05-20). Two reasons: 1. **Adoption signal:** sehawq.db v5 has ~9 NPM downloads per week. Multiple weeks of engineering investment is hard to justify against that baseline. 2. **Positioning:** nookdb is a *spiritual successor*, not a drop-in upgrade. A polished one-command importer would suggest otherwise; a short documented script is honest about the scope shift. The PRD §11 revised text and this page replace the original importer plan. ## What's different | sehawq.db v5 | nookdb | | ------------ | ------ | | Single-file JSON + `.log` append | redb (Rust, ACID) single file | | No schema | Schema-first; types and validation are first-class | | Built-in REST API server | Not shipped (out of scope for v1) | | Built-in admin dashboard | Not shipped | | Server-to-server replication | Not shipped (CRDT/sync may come in v2) | | GDPR helpers / audit log | Not shipped | | Webhook subsystem | Not shipped | If you depended on any of the "not shipped" features, nookdb is not a drop-in replacement. Stay on sehawq.db v5 or pick a different tool. ## Manual migration The repo ships a 50-line conversion script at [`examples/migrate-from-sehawq-v5/`](https://github.com/nookwright/nookdb/tree/main/examples/migrate-from-sehawq-v5). It expects a v5 JSON export with the most common shape: ```json { "version": 5, "data": { "users::alice": { "id": "alice", "name": "Alice" }, "posts::p1": { "id": "p1", "authorId": "alice" } } } ``` Usage: ```bash tsx convert.ts ./sehawq.json ./your-schema.ts ./out.db ``` Your `your-schema.ts` must export a default schema whose collection names match the key prefixes (`users`, `posts` in the example above). ## Limitations - The `.log` (WAL) file is **not** replayed. If you have unflushed writes in `.log`, run sehawq.db v5 one last time to flush, then export. - No schema inference. You must supply a schema with the expected fields. - Records that fail schema validation are skipped (with a count reported at the end). - The script writes through `nookdb`'s public `db[coll].insert(...)` API, so secondary indexes are built incrementally — there is no separate reindex step. ## Disposing of v5 Once your data is in nookdb, mark the old package deprecated on NPM: ```bash npm deprecate sehawq.db "Successor: nookdb. See https://nookdb.pages.dev/guides/migrating-from-sehawq-v5/" ``` Keep the old package published indefinitely for archival reasons; only the deprecation notice is needed. --- # Errors Source: https://nookdb.pages.dev/reference/errors/ > NookError hierarchy and the [kind] message convention. Every error nookdb throws extends `NookError`, which extends `Error`. `instanceof` checks work end-to-end (the M5a `setPrototypeOf` hardening preserves the prototype chain across sub-ES2015 transpile targets). ## Hierarchy ```text Error └── NookError // any nookdb error ├── NookStorageError // I/O, disk, redb-level failures ├── NookCorruptionError // bad magic, CRC mismatch, truncated streams ├── NookConflictError // unique-index violation, restore-target-not-empty ├── NookTransactionError // commit/rollback failures ├── NookInvalidArgError // bad collection name, empty key, etc. ├── NookClosedError // op on a closed database ├── NookSchemaError // schema validation, schema hash mismatch ├── NookMigrationError // ledger corruption, migration runner errors └── NookPlatformError // missing precompiled binary for current platform ``` ## The `[kind] message` convention Errors that originate in the Rust core are emitted across the NAPI boundary with a `[kind]` prefix: ```text [storage] disk full while writing entries table [corruption] backup checksum mismatch [conflict] users.email = "ali@example.com" already exists [schema] backup schema hash mismatch ``` The TS surface parses the prefix and instantiates the matching subclass automatically — code that catches `NookCorruptionError` works whether the error originated in Rust or in TS. ## Catching ```ts try { await db.users.insert({ email: 'ali@example.com' }); } catch (err) { if (err instanceof NookConflictError) { console.warn('email already in use'); return; } if (err instanceof NookSchemaError) { console.error('schema validation failed:', err.message); return; } throw err; } ``` ## Stable kinds The `[kind]` slug is part of the stable public API. The same Rust enum variant maps to the same kind slug across nookdb releases. --- # API reference Source: https://nookdb.pages.dev/reference/api/ > Top-level exports from the nookdb package. This is a hand-written overview. Detailed type signatures are visible in your editor via the package's bundled `.d.ts` files. ## Top-level exports ```ts // Opening open, // (path, options?) → Database | SchemaDatabase // Schema DSL s, // schema-builder namespace toDescriptor, // schema → canonical JSON (used by the Electron bridge) // Backup / restore (freestanding orchestrator helpers) backupToPath, restoreFromPath, // Reactive LiveQuery, // class returned by collection.live(...) // Errors NookError, NookStorageError, NookCorruptionError, NookConflictError, NookTransactionError, NookInvalidArgError, NookClosedError, NookSchemaError, NookMigrationError, NookPlatformError, // Error mapping (rare — usually used internally) mapNativeError, } from 'nookdb'; ``` ## `Database` (bytes-only handle) Returned by `open(path)` without options. The minimal byte-level API: ```ts class Database { put(collection: string, key: Buffer, value: Buffer): Promise; get(collection: string, key: Buffer): Promise; delete(collection: string, key: Buffer): Promise; listCollection(collection: string): Promise<{ key: Buffer; value: Buffer }[]>; listCollectionNames(): Promise; backup(destPath: string): Promise; restore(srcPath: string, opts?: RestoreOptions): Promise; migrateStatus(): Promise<{ currentVersion: number; appliedCount: number }>; migrateRun(versions: number[]): Promise; migrateListApplied(): Promise; close(): void; } ``` ## `SchemaDatabase` (typed handle) Returned by `open(path, { schema })`. Every collection key from `TSchema` becomes a typed `Collection` proxy on the handle, plus the same `backup` / `restore` / `migrate*` / `listCollectionNames` / `close` methods. ```ts const db = await open('./app.db', { schema: { users: s.collection({ /* … */ }) } }); db.users.insert(/* … */); db.users.find(/* … */); db.backup(/* … */); db.close(); ``` ## Other packages - `@nookdb/electron/main`, `@nookdb/electron/preload`, `@nookdb/electron/renderer` — the multi-process bridge. - `@nookdb/react` — the `useLive` hook. - `@nookdb/cli` — the `nookdb` command-line binary. Each package has its own README with focused docs. --- # .nbkp format Source: https://nookdb.pages.dev/reference/nbkp-format/ > Wire layout of the portable backup file (format version 1). `.nbkp` is the portable backup format produced by `db.backup(...)` (and `nookdb backup`). The spec below is **frozen for format version 1**. Future versions are reserved for compression, encryption, or restructuring; restore refuses unknown versions. ## File layout ``` ┌────────────────────────────────────────────────────────────┐ │ HEADER │ │ magic[8] "NOOKBKUP" (ASCII) │ │ format_ver u16 = 1 │ │ created_ms u64 wallclock ms at backup time │ │ schema_present u8 0 = absent, 1 = present │ │ schema_hash[32] SHA-256 of canonical schema descriptor │ │ (zero-filled when schema_present=0)│ │ redb_marker u32 informational (e.g. 0x02000000) │ │ entry_count_hint u64 informational; 0 = unknown │ ├────────────────────────────────────────────────────────────┤ │ ENTRIES (repeated) │ │ key_len u32 (must be > 0 for entries) │ │ key bytes (composite: "{coll}\0{user_key}") │ │ value_len u32 │ │ value bytes │ ├────────────────────────────────────────────────────────────┤ │ SENTINEL │ │ key_len = 0 u32 │ ├────────────────────────────────────────────────────────────┤ │ FOOTER │ │ crc32 u32 CRC32 over [HEADER ‖ ENTRIES ‖ │ │ SENTINEL] │ └────────────────────────────────────────────────────────────┘ ``` All multi-byte integers are big-endian. ## Field notes - **magic** — Eight ASCII bytes `NOOKBKUP`. Restore aborts with `NookCorruptionError` if the magic does not match. - **format_ver** — Currently `1`. Future versions are reserved; an unknown version triggers `NookCorruptionError: unsupported backup format version N`. - **schema_hash** — When the source DB has a known schema, the hash recorded here matches the schema-hash value the M4 Electron-bridge handshake uses. Restore validates this against the open DB's schema unless `skipSchemaCheck: true` is passed. A hash mismatch triggers `NookSchemaError`. - **redb_marker** — Informational only. The `.nbkp` format is independent of the redb on-disk layout; restore writes through the public storage API. - **entry_count_hint** — Informational. Restore enumerates until the sentinel; the hint is for progress reporting and does NOT affect correctness. - **CRC32** — Computed over everything up to but not including the footer. Mismatch triggers `NookCorruptionError: backup checksum mismatch`. A truncated stream triggers `NookCorruptionError: truncated backup stream`. ## Restore semantics - Default: target DB must be empty. Non-empty DB without `allowOverwrite: true` triggers `NookConflictError`. - With `allowOverwrite: true`: existing entries are cleared in the same write transaction that loads the backup. All-or-nothing. - Secondary indexes are NOT in the backup. After a restore the indexes will be rebuilt lazily by writes; if you need eager indexing, perform a follow-up scan (out of scope for v1). ## Practical usage The format is stable but opaque — `.nbkp` files are intended to be consumed by `nookdb restore` or the programmatic `restoreFromPath`. Inspecting them by hand is possible (it's binary but small), but should not be necessary in normal use. --- # Architecture overview Source: https://nookdb.pages.dev/architecture/overview/ > How the Rust core, NAPI binding, TS surface, and Electron bridge fit together. NookDB is four layers stacked on `redb`. Each layer has a single responsibility; data flows downward through the Rust core and back up through typed surfaces. ## Layers ### Rust core (`crates/nookdb-core`) Pure Rust, no NAPI dependency. Owns: - The schema IR and validator (`schema/`) - The composite-key codec (`codec/`) - The redb-backed storage layer (`storage.rs`) - The post-commit notifier (`notify.rs`) - The reactive engine (`live.rs`) - The migration runner (`migrate.rs`) - The backup/restore module (`backup.rs`) Testable without Node. Has its own integration tests (~80 tests) plus proptest suites. ### NAPI binding (`crates/nookdb-napi`) Bridges the Rust core to JavaScript. Produces a `.node` binary per platform via napi-rs. Six platforms are shipped via per-platform NPM packages (`@nookdb/core-`). `tokio::task::spawn_blocking` adapts the synchronous redb storage layer to JS-side async; errors are reshaped at the FFI boundary into a `[kind] message` prefix convention so the TS layer can re-hydrate them into the typed `NookError` hierarchy. ### TS surface (`packages/nookdb`) Wraps the NAPI binding with typed surfaces. Schema inference, error mapping, and the `LiveQuery` class live here. The Rust core is the **authoritative** validator — the TS surface trusts it. ### Electron bridge (`packages/electron`) A thin transport layer on top of the TS surface: - `@nookdb/electron/main` — `openHost()` and `connectPort()` for the Host process. - `@nookdb/electron/preload` — `exposeNookBridge()` to forward a MessagePort. - `@nookdb/electron/renderer` — `connectNook()` returning a typed proxy. Wire protocol is structured-clone over `MessageChannel`. Schema-hash handshake rejects mismatched schemas on connect. Per-renderer subscription registry cleans up on disconnect. ## What's NOT shipped No HTTP server, no admin dashboard, no replication, no CRDT, no GDPR helpers, no audit log. These were sehawq.db v5 features deliberately dropped — see PRD §14 for the rationale and the [Migrating from sehawq.db v5 guide](/guides/migrating-from-sehawq-v5/). ## Text-only diagram For copy-paste into issues, READMEs, or LLM prompts where SVG isn't viable:
Show ASCII version ```text ┌──────────────────────────────────────────────────┐ │ TypeScript API (npm package `nookdb`) │ │ open() / schema / collection / live / tx │ │ + @nookdb/electron bridge (main↔renderer) │ └─────────────────────┬────────────────────────────┘ │ NAPI-rs FFI ┌─────────────────────▼────────────────────────────┐ │ Rust core (native addon, .node binary) │ │ • Schema validator + type registry │ │ • Query planner + secondary index engine │ │ • Reactive notifier (post-commit hooks) │ │ • Transaction manager (MVCC snapshots) │ │ • Backup / restore + migration runner │ └─────────────────────┬────────────────────────────┘ │ ┌─────────────────────▼────────────────────────────┐ │ redb (storage engine — ACID + MVCC, single-file)│ └──────────────────────────────────────────────────┘ ```
## Where to next - See [Quick start](/quick-start/) to spin up a database. - See the [Electron bridge guide](/guides/electron-bridge/) for the main↔renderer setup. - See the [Backup / restore guide](/guides/backup-restore/) for snapshot management. --- # NookDB vs RxDB Source: https://nookdb.pages.dev/compare/rxdb/ > A short, honest comparison of NookDB and RxDB. ## Pick RxDB if... - You need your database to run in the browser, Node.js, or React Native simultaneously. - Server sync is a core requirement and you want a battle-tested replication protocol. - Your team is already deep in the RxJS ecosystem and reactive streams are how you think. ## Pick NookDB if... - You are building an Electron desktop app and the database lives only on that machine. - You need Rust-backed ACID durability without a cloud dependency in the critical path. - Schema-first TypeScript types matter more than an observable pipeline. ## Where they overlap Both RxDB and NookDB treat the schema as the source of truth: you declare your collections and field types up front, and the library enforces them at runtime. Both expose reactive query primitives so your UI can re-render when data changes without manual polling. Both are embedded — no separate database server process to manage. If you have tried one, the mental model of the other will feel familiar within an afternoon. ## What's genuinely different RxDB's architecture is built around sync. Its storage layer is an abstraction over pluggable backends (IndexedDB, SQLite, LokiJS), and the replication protocol is a first-class citizen. NookDB does not do sync at all — deliberately. The storage layer is a single Rust crate built on redb, a memory-mapped B-tree with crash-safe writes. That constraint lets NookDB make stronger per-write durability guarantees than any pluggable-backend design can make uniformly. If your workload is one machine, no cloud, NookDB keeps that surface area small. --- *— Ömer, Nookwright* --- # NookDB vs Jazz Source: https://nookdb.pages.dev/compare/jazz/ > A short, honest comparison of NookDB and Jazz. ## Pick Jazz if... - You want multi-device sync out of the box without building a backend yourself. - Your app is full-stack and you want one framework to span client, server, and the space in between. - CRDT-based conflict resolution fits your collaborative data model. ## Pick NookDB if... - Your app runs on a single machine and never needs to merge changes from another device. - You want no cloud dependency in the critical path — not even a relay server. - You think in typed schemas, not CRDT documents. ## Where they overlap Both Jazz and NookDB are "local-first" in the practical sense that they store data on the user's machine and let the UI read from that local store without a round-trip to a server. Both treat developer experience seriously: Jazz ships a full TypeScript SDK; NookDB ships a schema DSL where every field type is reflected in the query return type. If you care about keeping data close to the user and not paying per-query latency, both projects share that philosophy. ## What's genuinely different Jazz is a distributed system at heart. Its CRDT layer is designed for the reality that the same document may be edited on a phone, a laptop, and a web app simultaneously, and those edits need to merge without losing data. That is powerful, but it comes with a required network topology — Jazz Cloud or a self-hosted relay. NookDB makes the opposite bet: no network, no merge, one machine. The storage layer is a Rust B-tree with append-only writes and crash-safe recovery. If your Electron app is the only writer, that simplicity is a feature. --- *— Ömer, Nookwright* --- # NookDB vs Dexie Source: https://nookdb.pages.dev/compare/dexie/ > A short, honest comparison of NookDB and Dexie. ## Pick Dexie if... - Your app runs entirely in the browser and IndexedDB is the right storage layer. - You want a mature, well-documented library with years of production use behind it. - Your data model is simple enough that a lightweight IndexedDB wrapper is all you need. ## Pick NookDB if... - You are shipping an Electron desktop app and need storage that outlives the browser context. - A Rust storage engine with ACID crash safety is worth the extra dependency weight. - You need a multi-process bridge so renderer and main process share the same database without coordination code. ## Where they overlap Both Dexie and NookDB provide a schema-declaration layer that sits in front of the underlying storage engine, and both generate typed query results from that schema. Both support indexing fields for fast lookups and both expose a promise-based async API that feels natural in TypeScript. If you have built something with Dexie, the NookDB schema DSL will look familiar — `s.string()`, `s.number()`, `s.date()` mirror the same kinds of field builder patterns Dexie developers are used to. ## What's genuinely different Dexie wraps IndexedDB, which is a browser API. That makes it a natural fit for web apps and PWAs, but IndexedDB is not available in Electron's main process, and its durability guarantees reflect browser-first priorities rather than desktop-app priorities. NookDB's storage layer is a Rust crate compiled to a native `.node` binary — it uses memory-mapped files and append-only writes to give you crash-safe durability that survives a forced kill. It also ships a main-process bridge so renderer windows can issue queries without each window opening its own file handle. --- *— Ömer, Nookwright* --- # NookDB vs better-sqlite3 Source: https://nookdb.pages.dev/compare/better-sqlite3/ > A short, honest comparison of NookDB and better-sqlite3. ## Pick better-sqlite3 if... - SQL is the right query language for your workload and you want full expressive power. - You have heavy analytical queries — aggregations, joins, window functions — where SQL shines. - You prefer a synchronous API and are comfortable managing transactions in raw SQL strings. ## Pick NookDB if... - You want a typed schema DSL instead of writing SQL strings by hand. - Live reactive subscriptions (`live()`) are a requirement, not a nice-to-have. - You want a multi-process Electron bridge built into the library rather than built by you. ## Where they overlap Both better-sqlite3 and NookDB are embedded native libraries for Node.js — no separate database server, no TCP connection, no network latency. Both compile to a `.node` binary and are designed for Electron's main process. Both support transactions and give you crash-safe durability on modern file systems. If raw performance is the benchmark, both are in the same tier: the overhead is native-to-JS boundary crossing, not query planning. ## What's genuinely different better-sqlite3 exposes SQLite directly, which means your schema lives in SQL DDL strings and your queries are SQL text. That is a deliberate choice and a good one for teams who want the full power of a relational query engine. NookDB makes the opposite bet: no SQL, no strings, all types. The schema DSL produces TypeScript types at definition time, and the query API (`find`, `findOne`, `insert`, `update`, `delete`) is fully typed from those definitions. The trade-off is that NookDB's query language is intentionally narrower than SQL — but it ships `live()` reactive subscriptions and a multi-process Electron bridge as first-class primitives that you would otherwise have to build yourself on top of better-sqlite3. --- *— Ömer, Nookwright*