Quick start
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
pnpm add nookdbnpm install nookdbyarn add nookdbbun add nookdbnookdb ships precompiled native binaries for Win/macOS/Linux × x64/arm64. No build toolchain required.
Define a schema
import { s } from 'nookdb';
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
import { open } from 'nookdb';import { schema } from './schema';
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:
pnpm add nookdb @nookdb/electronnpm install nookdb @nookdb/electronyarn add nookdb @nookdb/electronbun add nookdb @nookdb/electronimport { MessageChannelMain } from 'electron';import { openHost } from '@nookdb/electron/main';import { schema } from '../shared/schema';
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]);import { exposeNookBridge } from '@nookdb/electron/preload';exposeNookBridge(); // forwards a MessagePort to window.nookdbimport { connectNook } from '@nookdb/electron/renderer';import { schema } from '../shared/schema';
const db = await connectNook({ schema });const admins = await db.users.find({ role: 'admin' }); // same API, typedThe 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 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:
import { NookSchemaError, NookConflictError } from 'nookdb';
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.
Next steps
- Browse the Schema DSL guide to see every builder.
- Try Reactive
live()for UI that stays in sync with the database. - Read the Architecture overview for how the Rust core, NAPI binding, and Electron bridge fit together.