Connect from Node.js

Last updated 29 October 2026

View .md

Connect a Node.js program to a database once you have created one: open it by name with an API key, commit, and await synced() before anything outside the process acts. For Python, see Connect from Python.

open() from @tonbo/sqlite-ursula/tonbo opens a database by its name, with a Tonbo API key, and returns a node:sqlite DatabaseSync, so a library that takes one takes it, such as Drizzle ORM (see Use with Drizzle). Install the package as the SQLite quickstart does.

app.mjs
import { open } from "@tonbo/sqlite-ursula/tonbo";

const db = await open("notes", {
  dir: "./notes-cache", // the cache root
});
db.exec("CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT)");
db.prepare("INSERT INTO notes(body) VALUES (?)").run("hello");
await db.synced();
TONBO_API_KEY=tbo_... node app.mjs

The key comes from the apiKey option, else TONBO_API_KEY, else the login tonbo login saved, so locally, after tonbo login, plain node app.mjs is enough. The package exchanges the key for short-lived access tokens and keeps one fresh in the background. See Opening by name for the order and the errors.

Requirements

  • Node.js 22.19 or later, in a process that keeps the database open while it works. A short-lived process works too, and rebuilds the local copy at each start (see Short-lived processes). See Where it runs for the machines that fit.
  • One of the prebuilt platforms: Linux x64 or arm64 with glibc 2.28 or later, Linux x64 with musl (Alpine), or macOS 11 or later on Apple silicon. Elsewhere, build the extension from source and set SQLITE_URSULA_VFS, as the package README describes.
  • A local disk for the cache directory, the dir option, also called the cache root, not a network filesystem.
  • @tonbo/sqlite-ursula pinned to the exact version these docs name (--save-exact). open sets upWAL mode with 4 KiB pages; do not change journal_mode or the page size.

open()

The first open of a database in a process looks its name up through the Management API, the API at https://api.tonbo.dev that creates, lists and deletes databases, waits while it is still being created, catches its local copy up from Tonbo and makes this process the database’s writer. Pass dir to keep the local copy in a directory of your own, and signal to bound the open (30 seconds by default). Every option, with its default, is on open() in the Node.js SDK reference.

Use one key per process: the process holds one key for its access tokens, so every database it opens is reached as that key’s account, and an open by name with a second key fails with UrsulaSetupError invalid_argument (see One token per process).

synced()

A commit returns once it is on this host. await db.synced() resolves once Tonbo holds every commit made before the call. Await it before anything outside the process acts on what you wrote or read, such as a response or a queue acknowledgement. The package’s gate(db, work) does that for a handler: it settles with work’s outcome only once a synced() made after it resolved. See When a commit is durable. To make a call to another service once across crashes and second instances, see Run side effects once, and for the other rules, Rules for databases.

Connections

Each later open of the database in the process returns another connection to it, and concurrent opens share one. A later open by URL needs no network; one by name still looks the name up. Worker threads call open too. The connections share the local copy’s files, so node:sqlite’s busy timeout defaults to 5000 ms. db.close() is synchronous: once the last connection closes, the commits left replicate in the background. Open the database again with open().

Open on another host

Stop the process on the old host, then open the same database on the new one, with the same key. The open rebuilds the local copy from Tonbo (see Cold starts) and makes the new process the writer. If the old process still runs, its commits are refused from then on: its statements and synced() throw UrsulaResetError with the kind fenced. Do not copy the cache root to another host. On Kubernetes, use the Recreate strategy or a StatefulSet, so that the new pod starts after the old one stops.

Status

db.status() reports the database’s state. The fields to watch:

  • commits.pending and commits.pending_bytes: commits on this host that Tonbo does not hold yet.
  • replication.condition: why commits wait, such as unauthorized or payment_required, or null.
  • attach.local: where the open got the local copy, such as resumed from the cache root or built from Tonbo.
  • reset: null until the database is reset, then its kind and action.
  • salvage: local copies an open moved aside (see Copies an open moved aside).

Status lists every field, and Troubleshooting says what to do about each.

Open by URL

open also takes the database’s URL, which skips the name lookup. Every open by name asks the Management API, so while that API does not answer, open the database by its URL; the access token still comes from the key.

const db = await open("https://db.tonbo.dev/<account-id>/notes", {
  dir: "./notes-cache",
});

Another opener

For Pi Durable, openUrsulaPiStorage from @tonbo/sqlite-ursula/tonbo/pi opens a database by name in one call (see Quickstart: Pi Durable). For any other opener that takes a URL and a token, resolveDatabase looks the name up without opening the database. It finds the key and fails as open does:

import { resolveDatabase } from "@tonbo/sqlite-ursula/tonbo";

// url is the database's URL; token is undefined while the key's own tokens serve,
// which the extension keeps fresh: pass it on as it is.
const { url, token } = await resolveDatabase("notes");

The token file

Instead of a key, a process can hold an access token in a file, as the token refresher keeps it, and open databases by URL. Set URSULA_VFS_TOKEN_FILE to the file: the extension reads the variable once, at the first call, so it must be in the environment when the process starts. It reads the file again whenever the file changes and after Tonbo refused a token, so the refresher can replace it while the database is open:

URSULA_VFS_TOKEN_FILE="$HOME/.tonbo/streams-token" node app.mjs

An explicit token option or URSULA_VFS_TOKEN_FILE serves the database requests in place of the key, and opening a URL then needs no key.