> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# 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](https://tonbo.dev/docs/databases/python.md).

`open()` from `@tonbo/sqlite-ursula/tonbo` opens a database by its name, with a Tonbo API key, and returns a [`node:sqlite`](https://nodejs.org/docs/latest-v22.x/api/sqlite.html) [`DatabaseSync`](https://nodejs.org/docs/latest-v22.x/api/sqlite.html#class-databasesync), so a library that takes one takes it, such as Drizzle ORM (see [Use with Drizzle](https://tonbo.dev/docs/databases/drizzle.md)). Install the package as the [SQLite quickstart](https://tonbo.dev/docs/getting-started/quickstarts/sqlite.md#install) does.

**app.mjs**

```js
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();
```

```shell
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](https://tonbo.dev/docs/platform/api-keys.md#open-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](https://tonbo.dev/docs/databases/cold-starts.md#short-lived)). See [Where it runs](https://tonbo.dev/docs/databases.md#where) 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](https://www.npmjs.com/package/@tonbo/sqlite-ursula) 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 up[WAL mode](https://sqlite.org/wal.html) 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()](https://tonbo.dev/docs/reference/nodejs/open.md#tonbo-open-options) in the [Node.js SDK reference](https://tonbo.dev/docs/reference/nodejs.md).

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](https://tonbo.dev/docs/reference/nodejs/tokens.md#one-token)).

## 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](https://tonbo.dev/docs/databases/how-it-works.md#commit). To make a call to another service once across crashes and second instances, see [Run side effects once](https://tonbo.dev/docs/databases/side-effects.md), and for the other rules, [Rules for databases](https://tonbo.dev/docs/databases/rules.md).

## 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](https://tonbo.dev/docs/databases/cold-starts.md)) 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](https://tonbo.dev/docs/databases/backups.md#salvage)).

[Status](https://tonbo.dev/docs/reference/nodejs/status.md) lists every field, and [Troubleshooting](https://tonbo.dev/docs/databases/troubleshooting.md) 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.

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

## Another opener

For [Pi Durable](https://github.com/earendil-works/pi/blob/main/packages/durable/README.md), `openUrsulaPiStorage` from `@tonbo/sqlite-ursula/tonbo/pi` opens a database by name in one call (see [Quickstart: Pi Durable](https://tonbo.dev/docs/getting-started/quickstarts/pi.md)). 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:

```js
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](https://tonbo.dev/docs/platform/api-keys.md#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:

```shell
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.
