Skip to content

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

Terminal window
pnpm add nookdb

nookdb 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:

Terminal window
pnpm add nookdb @nookdb/electron
main.ts
import { 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]);
preload.ts
import { exposeNookBridge } from '@nookdb/electron/preload';
exposeNookBridge(); // forwards a MessagePort to window.nookdb
renderer.ts
import { 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, 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 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