Skip to content

Choose branch fork or new database

PerSQL offers three isolation primitives for agent runs: a branch (schema-only, ephemeral), a fork (named copy, optional data), or a new database (blank, permanent). The choice determines cost, blast radius, and lifecycle. This guide maps each to its correct use case and shows the exact SDK calls to provision them.

Use a branch when an agent run needs the parent’s schema but must not touch production rows — CI pipelines, sub-agent sandboxes, and PR previews fit here. A branch is idempotent on a ref and auto-deletes on a TTL, so a run that crashes mid-flight leaves nothing behind. Use a fork when you need a persistent named database for inspection — copying a production schema into a staging environment where humans will browse data later, or spinning up a read-only analytics replica from a snapshot. Forks are also the right choice when an agent experiment produces data that another service needs to query through a stable URL. Use a new database for long-lived tenant isolation: each user or team gets their own blank DO with no parent lineage, metered independently and kept until explicitly deleted. New databases are also the right choice when the agent’s schema diverges permanently from any existing parent, or when you need a fully independent blast radius for a customer-facing feature.

import { PerSQL } from "@persql/sdk";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN! });
const db = persql.database("acme", "orders");
// Verify parent schema before deciding isolation strategy
const tables = await db.tables();
const hasEvents = tables.some(t => t.name === "events");
if (!hasEvents) {
await db.query("CREATE TABLE events (id INTEGER PRIMARY KEY, ref TEXT, payload TEXT)");
await db.query("CREATE INDEX idx_events_ref ON events(ref)");
}
// Branch: ephemeral sandbox inheriting schema only
const br = await db.branches.upsert("run-42", { ttlDays: 1 });
// Fork: persistent named copy, optionally with rows
const fk = await db.fork({ name: "Staging", slug: "orders-staging", includeData: true });
// New database: blank permanent DO for a tenant
const nd = await persql.databases.create({ name: "Tenant 7", slug: "tenant-7" });

Do not use a branch when the output must survive past its TTL — branches auto-delete and take their data with them. Do not fork a large database with includeData: true if you only need schema, because you pay metered storage for every copied row from the moment the fork finishes. Do not create a new database for every single transient agent run: each DO incurs storage charges from the first byte and requires explicit deletion, so a runaway loop that spawns thousands of blank databases will drain a prepaid balance even if most stay empty. Finally, do not branch or fork if the parent schema is still in flux; schema drift between parent and child forces manual reconciliation that a single shared database avoids. Branches and forks assume the parent schema is relatively stable. Also, do not fork when you need real-time consistency with the parent; a fork is a point-in-time copy, not a replica.

A branch is the cheapest option for ephemeral work because it copies only schema objects, not rows. The cost is essentially zero at creation time; you pay only for the requests and rows the branch itself writes, and the auto-delete TTL caps storage exposure. The tradeoff is rigidity: branches are meant for short-lived runs, so they are wrong for anything that must persist. They also inherit the parent’s schema exactly, which means schema changes on the parent do not flow backward to active branches; if you need a branch that tracks a moving parent, you must delete and recreate it. Compared to a new database, a branch saves the agent from running CREATE TABLE migrations, but it also means the agent cannot change the schema without affecting the parent or breaking its own assumptions.

A fork is the middle ground — you get a named, permanent database with an independent lifecycle, and you can optionally carry the parent’s rows forward. The cost is storage for every copied row plus the one-time write charge during the copy. Forks also give you a clean URL and slug, which matters when humans or other services bookmark the database. However, a fork with includeData: true is best-effort for a live source; concurrent writes on the parent can create referential orphans that the fork prunes automatically, so an exact image requires quiescing the parent first. Compared to a branch, a fork is more expensive but survives indefinitely; compared to a new database, a fork is faster to provision because the schema is already present.

A new database is the most isolated and the most expensive relative to its starting utility: it begins completely blank, so the agent must build schema from scratch, but there is no parent to drift against and no shared failure domain. If the parent database is corrupted or deleted, a branch or fork can be affected by lineage confusion; a new database is fully independent and can be moved between namespaces or backed up without thinking about ancestors. New databases also carry no historical baggage: no obsolete indexes, no leftover migration tables, no baggage from early prototyping. The agent starts from a known state.

The cost model reinforces these distinctions. Branches pay nothing at creation because only schema objects are duplicated; storage meter starts when the branch writes its first row. Forks with includeData: true start paying storage immediately for every copied row. New databases pay for the empty page structure from the first CREATE TABLE, though this is typically under a megabyte and bounded. For an agent running hundreds of times per day, branches keep the baseline near zero; forks or new databases would create persistent storage that accumulates regardless of activity.

Compared to running everything against a single shared database, all three options add operational overhead: you now have multiple SQLite files to reason about, query logs are split across DOs, and there is no built-in cross-database join. But the shared-database alternative forces the agent to namespace its own tables or rows with synthetic tenant_id columns, which pushes complexity into application code that SQLite could have handled through physical isolation. It also means one rogue write or schema change can affect every tenant simultaneously. Compared to raw API calls against a remote Postgres, all three give you embedded SQLite in a Durable Object — lower latency, no connection pool, and native JSON support — but you lose the mature migration ecosystem, larger data types, and extensive operator family of Postgres. Within PerSQL itself, branches beat forks for CI automation because they are idempotent and nearly free at rest; forks beat branches when a human or another agent needs to inspect the result days later or when the experiment produced data that downstream jobs consume through a stable URL; new databases beat both when the data owner changes, when schema lineage must be deliberately broken, or when regulatory or contractual requirements demand hard isolation boundaries. The right default for CI and per-agent sandboxes is a branch; the right default for multi-tenant user data or user-owned app databases is a new database; forks exist for the narrow band where you need a persistent, inspectable snapshot of an existing schema with optional seed data.

Per-agent sandbox