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

# Pi Durable storage

Last updated 29 October 2026

`openUrsulaPiStorage()` runs [Pi Durable](https://github.com/earendil-works/pi/blob/main/packages/durable/README.md)’s official node `SqliteStorage` on a replicated database.

It needs `@earendil-works/pi-durable`, an optional peer dependency, from 1.0.0 up to but not including 1.4.0. A walkthrough is the [Pi Durable quickstart](https://tonbo.dev/docs/getting-started/quickstarts/pi.md).

## openUrsulaPiStorage() by name

From `@tonbo/sqlite-ursula/tonbo/pi`, since 0.11.1.

```text
function openUrsulaPiStorage(
  nameOrUrl: string | URL,
  options?: TonboPiStorageOptions,
): Promise<SqliteStorage>

type TonboPiStorageOptions = UrsulaPiStorageOptions & {
  apiKey?: string;
  apiOrigin?: string;
  authOrigin?: string;
};
```

```js
import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/tonbo/pi";

const harness = await Harness.open(await openUrsulaPiStorage("harness-1"), { models, registry }, context);
harness.resume();
```

It resolves `nameOrUrl` as [resolveDatabase()](https://tonbo.dev/docs/reference/nodejs/resolve-database.md) does, then opens it with the `/pi` entry’s `openUrsulaPiStorage` below; the extension keeps the access tokens exchanged for the key fresh. A URL skips the lookup, and a missing `dir` names the default cache root. `Harness.close()` closes the storage. It throws as the `/tonbo` [open()](https://tonbo.dev/docs/reference/nodejs/open.md#tonbo-open) does, and the entry re-exports those errors.

`TonboPiStorageOptions` are the options below plus `apiKey`, `apiOrigin` and `authOrigin`, as [TonboOpenOptions](https://tonbo.dev/docs/reference/nodejs/open.md#tonbo-open-options) has them.

## openUrsulaPiStorage() by URL

From `@tonbo/sqlite-ursula/pi`.

```text
function openUrsulaPiStorage(
  url: string | URL,
  options?: UrsulaPiStorageOptions,
): Promise<SqliteStorage>

type UrsulaPiStorageOptions = {
  dir?: string;
  token?: string | TokenProvider;
  signal?: AbortSignal;
  maxPendingBytes?: number;
  recreate?: boolean;
  walAutoCheckpointPages?: number;
  busyTimeoutMs?: number;
  closeTimeoutMs?: number;
};
```

```js
import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/pi";
import { resolveDatabase } from "@tonbo/sqlite-ursula/tonbo";

// token is undefined while the key's own tokens serve: pass it on as it is.
const { url, token } = await resolveDatabase("harness-1");
const storage = await openUrsulaPiStorage(url, { token, dir: "./harness-1-cache" });
```

It opens the stream at `url` with the root [open()](https://tonbo.dev/docs/reference/nodejs/open.md#root-open), then runs Pi’s node driver, unmodified, on the local file, and resolves with Pi’s `SqliteStorage`. To open a Tonbo database by name, pass what [resolveDatabase()](https://tonbo.dev/docs/reference/nodejs/resolve-database.md) hands out.

- Every transaction, and every statement outside one, resolves only once `synced()` did: a Pi commit resolves once the stream holds it, and a read once what it read is in the stream.
- A commit refused with `backlog_full` commits nothing. From Pi 1.2, the storage waits for `synced()` and runs it again. On Pi 1.0 and 1.1 it throws Pi’s `StorageRejected`, whose `cause` is the `UrsulaWriteRefusedError`, so the session continues.
- A reset throws the [UrsulaResetError](https://tonbo.dev/docs/reference/nodejs/errors.md#ursula-reset-error) itself, unwrapped, so the session stops. See [Resets in a Harness](#resets). Other failures throw the package’s other errors.
- `close()` waits up to `closeTimeoutMs` for `synced()`, then closes. What the stream lacks then stays in the local log for the next open.

## Resets in a Harness

When the storage throws an `UrsulaResetError`, the Harness’s session stops. What to do follows the error’s `action`:

| Action | Kinds | What to do |
| --- | --- | --- |
| `reopen` | `stale`, `rewound`, `local_damage`, `internal` | Close the storage, then `openUrsulaPiStorage()` again and resume the session from it. |
| `stop` | `fenced` | Another process took the database over. Stop this Harness and alert: reopening in a loop takes the database back from the new owner. |
| `fix` | `deleted`, `stream_state` | Fix the cause first, such as a deleted database, then open the storage again. |

## Options

`UrsulaPiStorageOptions`. It takes no `readOnly` and no other `DatabaseSync` option.

| Option | Default | What it does |
| --- | --- | --- |
| `dir` | The user cache | As [open()](https://tonbo.dev/docs/reference/nodejs/open.md#option-dir)’s. |
| `token` | None: `URSULA_VFS_TOKEN_FILE` | As [open()](https://tonbo.dev/docs/reference/nodejs/open.md#option-token)’s. |
| `signal` | 30 seconds | Bounds the open, as [open()](https://tonbo.dev/docs/reference/nodejs/open.md#option-signal)’s. |
| `maxPendingBytes` | 64 MiB | As [open()](https://tonbo.dev/docs/reference/nodejs/open.md#option-max-pending-bytes)’s. |
| `recreate` | `false` | As [open()](https://tonbo.dev/docs/reference/nodejs/open.md#option-recreate)’s. |
| `walAutoCheckpointPages` | Pi's | Passed to Pi's node driver. |
| `busyTimeoutMs` | Pi's | Passed to Pi's node driver. |
| `closeTimeoutMs` | 10 seconds | How long the storage’s `close()` waits for `synced()` before it closes anyway. |
