Connect from Node.js
Last updated 29 October 2026
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.
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.mjsThe 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
diroption, also called the cache root, not a network filesystem. @tonbo/sqlite-ursulapinned to the exact version these docs name (--save-exact).opensets upWAL mode with 4 KiB pages; do not changejournal_modeor 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.pendingandcommits.pending_bytes: commits on this host that Tonbo does not hold yet.replication.condition: why commits wait, such asunauthorizedorpayment_required, ornull.attach.local: where the open got the local copy, such asresumedfrom the cache root orbuiltfrom Tonbo.reset:nulluntil the database is reset, then itskindandaction.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.mjsAn 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.