Rules: persist, then act
Last updated 29 October 2026
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.
db.prepare("INSERT INTO orders(id, item) VALUES (?, ?)").run(order.id, order.item);
await db.synced(); // Tonbo holds the order
reply("ordered");db.prepare("INSERT INTO orders(id, item) VALUES (?, ?)").run(order.id, order.item);
reply("ordered"); // the order is only on this machineLet 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.
import { gate, open } from "@tonbo/sqlite-ursula/tonbo";
const db = await open("notes");
const handler = (request) => gate(db, () => router.fetch(request), { signal: request.signal });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.
// 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),
);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 callBound 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()).
await db.synced({ signal: AbortSignal.timeout(10_000) }); // rejects after 10 secondsawait db.synced(); // in a request handler: waits as long as Tonbo is unreachable