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

CallEffect
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.

Next

Twill DB documentation · Licensed under BUSL-1.1. · Author