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

# Tokens

Last updated 29 October 2026

How the requests to a stream get their bearer token: the `token` option, a token provider, and the token file.

On Tonbo, the token is an access token exchanged for an API key (see [The token exchange](https://tonbo.dev/docs/platform/api-keys.md#exchange)). Through the `/tonbo` entry the extension makes that exchange for you and keeps the token fresh; the root entry takes a token you supply.

## Where the token comes from

1. The `token` option of the last [open()](https://tonbo.dev/docs/reference/nodejs/open.md#option-token) that names one: a string, or a provider.
2. Through `@tonbo/sqlite-ursula/tonbo` without a `token`, unless `URSULA_VFS_TOKEN_FILE` is set: the API key, which the extension exchanges for Streams tokens and refreshes on a thread of its own, at 70 to 80% of each token’s lifetime and at once after a `401`. A failed refresh backs off from 1 second up to 30 while the last token serves. An open by name, or [resolveDatabase()](https://tonbo.dev/docs/reference/nodejs/resolve-database.md), makes the key the source again after an open with a `token` replaced it. This is so since 0.11.2; 0.11.1 ran a token provider for the key.
3. `URSULA_VFS_TOKEN_FILE`, which the extension reads only while no open of the process has set a token (see [The token file](#token-file)).

No token is ever logged, and no error message carries one.

## TokenProvider

Exported by `@tonbo/sqlite-ursula` and `/tonbo`, since 0.11.1. Before it, `token` took a string only.

```text
type TokenProvider = (options: { signal: AbortSignal }) => Promise<BearerToken>;

interface BearerToken {
  token: string;
  expiresAt: Date | number; // milliseconds since the epoch
}
```

```js
import { open, UrsulaUnavailableError } from "@tonbo/sqlite-ursula";

const token = async ({ signal }) => {
  const r = await fetch(exchangeUrl, {
    method: "POST",
    headers: { authorization: `Bearer ${apiKey}` },
    signal,
  });
  if (r.status === 401) {
    // Fails for good: the package stops calling the provider.
    throw new UrsulaUnavailableError("unauthorized: the exchange refused the key", "unauthorized");
  }
  if (!r.ok) throw new Error(`the exchange answered ${r.status}`); // retried
  const { access_token, expires_in } = await r.json();
  return { token: access_token, expiresAt: Date.now() + expires_in * 1000 };
};

const db = await open(url, { token });
```

- The package calls the provider before an open’s first request when it holds no valid token, again after 70 to 80% of each token’s lifetime, and again at once when a request was refused with `401`. Concurrent calls share one.
- `signal` fires when the package stops waiting for the answer: after 30 seconds, or once an open with a string token stopped the provider or no database uses it.
- To fail for good, throw an [UrsulaError](https://tonbo.dev/docs/reference/nodejs/errors.md#ursula-error) whose `action` is `fix`: an open waiting for its token rejects with it, and the package stops calling the provider until the next `open()` with a provider. Every other error is retried with backoff, from 1 second up to 30, while the token held keeps serving.
- An open gets a valid token the package holds at once, so a connection on a database already bound needs no network. An expired token serves only while the provider is failing; otherwise the open waits for a new one.
- When an open’s signal fires while it waits for the provider, it rejects with `UrsulaUnavailableError` `unavailable`, whose `cause` is the provider’s last error, or the signal’s reason when it did not answer.

## BearerToken

What a provider hands out, since 0.11.1.

| Field | Type | What it holds |
| --- | --- | --- |
| `token` | `string` | The token: printable ASCII without spaces. |
| `expiresAt` | `Date` or `number` | When it expires: a `Date`, or milliseconds since the epoch. A number below 1012 reads as seconds, such as a JWT’s `exp`, and is refused: pass `exp * 1000`. |

A token that is not printable ASCII, or an `expiresAt` that is not a time, fails the open with `UrsulaSetupError` `invalid_argument`. A token that expires within 1 second leaves no time to refresh it, and is retried as a failure.

## One token per process

- The process carries one token, whichever function minted it, so give it one account’s token: every database it opens is reached as that account. The last open that names a token sets it.
- One API key serves a process’s Streams tokens (since 0.11.2): once the first key held a token, an open by name without a `token` and with a second key fails with `UrsulaSetupError` `invalid_argument` while any database of the process may send requests, and for 60 seconds after an open was handed the first key’s token. A second key with a `token`, or with `URSULA_VFS_TOKEN_FILE`, only looks the name up and is never refused.
- An open with another provider, an inline arrow included, shares the token held and the call in flight, and names the function the next call runs. An open with a string stops the provider on its thread, and the opens waiting for it go on under the string.
- A provider lives on one JavaScript thread, so each worker thread runs its own: give the threads of a process the same kind of token.
- The provider’s timers keep the process alive only while an open waits for a token, and it stops once no database its thread opened is bound.

## When the server refuses a token

A request of a bound database refused twice with `401` blocks replication, and [replication.condition](https://tonbo.dev/docs/reference/nodejs/status.md#conditions) is `unauthorized`. The provider is called at once, or the API key’s token is refreshed at once, and the blocked request goes again as soon as the new token is set. Commits continue meanwhile, until the backlog fills. A provider failure after the open leaves the token held in place.

## The token file

`URSULA_VFS_TOKEN_FILE` names a file that holds the token, for a process that keeps its token in a file a separate refresher replaces. How to run it is on [Connect from Node.js](https://tonbo.dev/docs/databases/connect.md#token) and [The refresher](https://tonbo.dev/docs/platform/api-keys.md#refresher). [importSqlite()](https://tonbo.dev/docs/reference/nodejs/gate-and-import.md#import-options) takes a string token of its own, for the import alone.
