Embedded: Node & frameworks
@twilldb/node embeds the engine in-process on Node — function-call latency, no server, no socket — with the same API as @twilldb/bun. It binds the native engine through koffi instead of bun:ffi, so it runs on Node and every Node-based framework: Next.js, Astro, Nuxt, Remix, SvelteKit, Vite SSR.
Install
npm install @twilldb/node # or pnpm / yarn / bun add
The native engine ships as a per-platform optional dependency
(@twilldb/engine-<os>-<arch> — the same binaries @twilldb/bun uses), so there is no cargo build. To point at a libengine you built yourself, set TWILLDB_ENGINE_PATH=/abs/path/libengine.so.
Runtime requirements
Node ≥ 22.18 runs the published .ts sources directly (type stripping); on older Node, build with tsc/tsx. Keep the database on the Node.js runtime, not the Edge runtime — FFI is unavailable on Edge.
Quickstart
import { open } from "@twilldb/node";
// file:// embedded · s3://|r2://|gs:// storage-disaggregated
const db = open("file://./local.db");
try {
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
db.query("INSERT INTO notes (id, body) VALUES (?, ?)", [1, "hello"]);
const rows = db.query("SELECT id, body FROM notes");
console.log(rows);
} finally {
db.close();
}
Breakdown: the storage backend is chosen entirely by the URL scheme — your code is identical for file:// and s3://. Parameters bind positionally (?); values are never string-interpolated. The same snippet runs on Bun by changing the import to @twilldb/bun.
API at a glance
| Call | Effect |
|---|---|
open(url) | open a database (backend chosen by URL scheme) |
db.exec(sql) | run DDL/DML, returns rows affected |
db.query(sql, params?) | buffered rows; params bind positionally |
db.prepare(sql) | reusable statement (.all / .get / .run) |
db.transaction(fn) | BEGIN/COMMIT, rollback on throw; commit blocks until durable |
db.branch(name) | copy-on-write branch at the current LSN |
db.close() | release the handle (idempotent) |
Errors throw EngineError carrying the numeric status and a retryable flag (set for conflict / transient-storage failures). It is the same surface as the Bun client — see that page for the full reference.
Frameworks (Next.js, Astro, Vite, …)
A Node framework renders in a Node server process; the engine embeds in that same process. The one rule that matters: open one Database per process and reuse it across requests — the engine is a single writer per database, so a per-request open would thrash the WAL.
// lib/db.ts — a module-level singleton, reused across requests.
import { open, type Database } from "@twilldb/node";
const g = globalThis as unknown as { __twill?: Database };
export function db(): Database {
if (!g.__twill) {
g.__twill = open(process.env.TWILLDB_URL ?? "file://./app.db");
g.__twill.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
}
return g.__twill; // globalThis guard survives dev hot-reload
}
Next.js (App Router)
// app/api/notes/route.ts
import { db } from "@/lib/db";
export const runtime = "nodejs"; // FFI needs the Node runtime, not Edge
export async function GET() {
return Response.json(db().query("SELECT id, body FROM notes ORDER BY id DESC LIMIT 100"));
}
export async function POST(req: Request) {
const { body } = await req.json();
db().transaction((tx) => tx.query("INSERT INTO notes (body) VALUES (?)", [body]));
return Response.json({ ok: true }, { status: 201 });
}
Breakdown: export const runtime = "nodejs" is required — on the Edge runtime FFI is unavailable. The route reuses the shared handle; the transaction's COMMIT blocks until the WAL is durable.
Astro / SvelteKit / Nuxt / Remix / Vite SSR
The pattern is identical — only the route-handler signature changes. An Astro API route:
// src/pages/api/notes.ts
import { db } from "../../lib/db";
export const GET = () =>
new Response(JSON.stringify(db().query("SELECT id, body FROM notes")), {
headers: { "content-type": "application/json" },
});
Edge? Use server mode
If a route must run on an Edge runtime, it can't embed via FFI. Run engine-server and connect over Postgres with node-postgres / Bun.sql instead — see Server: Node / Bun.