Rules: persist, then act

Last updated 29 October 2026

View .md

Check these rules before your program replies, acknowledges or calls another service after a commit: each keeps a commit from being announced before Tonbo holds it.

The examples use the connection open() returns, a node:sqlite DatabaseSync, as Connect opens it.

Await synced() before anything outside the process acts

A commit returns once it is on this machine, and losing the machine loses it until synced() resolves (see When a commit is durable). A reply, an acknowledgement or a payment that goes out first can outlive the commit it reports.

Do
db.prepare("INSERT INTO orders(id, item) VALUES (?, ?)").run(order.id, order.item);
await db.synced(); // Tonbo holds the order
reply("ordered");
Don’t
db.prepare("INSERT INTO orders(id, item) VALUES (?, ?)").run(order.id, order.item);
reply("ordered"); // the order is only on this machine

Let gate() hold a handler’s answer

gate() settles with the work’s outcome only once a synced() made after the work resolved, so no path of the handler can forget it. Reads wait too: a result may have read another call’s commit that Tonbo does not hold yet.

Do
import { gate, open } from "@tonbo/sqlite-ursula/tonbo";

const db = await open("notes");
const handler = (request) => gate(db, () => router.fetch(request), { signal: request.signal });
Don’t
const handler = async (request) => {
  const response = await router.fetch(request);
  if (request.method !== "GET") await db.synced(); // a GET may have read an unsynced commit
  return response;
};

Record a side effect before you make it

Record the intent with an idempotency key derived from the task, await synced(), then make the call. A call made before its record repeats after a crash, and a random key looks new to the provider every time. The whole pattern, with recovery, is Run side effects once.

Do
// The same key on every run of this task; perform() records, syncs, then calls.
await perform(`task-${task.id}/welcome-email`, { to: task.email, template: "welcome" }, () =>
  db.prepare("UPDATE tasks SET step = 'welcomed' WHERE id = ?").run(task.id),
);
Don’t
await fetch("https://api.example.com/v1/emails", {
  method: "POST",
  headers: { "Idempotency-Key": crypto.randomUUID() }, // a new key on every run
  body: JSON.stringify({ to: task.email, template: "welcome" }),
});
db.prepare("UPDATE tasks SET step = 'welcomed' WHERE id = ?").run(task.id); // after the call

Bound a wait where someone is waiting

synced() waits through an outage instead of throwing, and the commits keep replicating. Where a caller waits for the answer, pass a signal so it rejects in time (see synced()).

Do
await db.synced({ signal: AbortSignal.timeout(10_000) }); // rejects after 10 seconds
Don’t
await db.synced(); // in a request handler: waits as long as Tonbo is unreachable