Data branching
Spin up an isolated branch of a tenant's data — a namespace snapshot/restore by default (dialect-agnostic, no vendor lock) or a Neon copy-on-write fast-path on Postgres — then bind to it and tear it down. The basis for per-PR preview environments.
Data branching creates an isolated copy of a tenant's data you can read and write without touching the source — the basis for per-PR preview environments and safe "try a migration against real-shaped data" workflows. The default mechanism is a namespace snapshot/restore (works on every dialect, no vendor lock); on Postgres backed by Neon, a copy-on-write branch fast-path is used instead.
The plan/execute split
Branching is a plan (what to do) + an executor (do it), mirroring the migration engine. The plan is pure + inspectable; the executor performs the I/O.
import { planBranch, resolveBranchMechanism } from '@voltro/database'
// Pick the mechanism: 'neon-cow' when seeding a copy on a Neon URL,
// 'namespace' (schema-per-branch) otherwise.
const mechanism = resolveBranchMechanism({ seed: 'copy', dbUrl: connectionString })
const steps = planBranch({
mechanism,
branchId: 'branch_acme_pr128',
tableNames,
seed: 'copy',
parentNamespace: 'public', // namespace mechanism: the schema to snapshot from
})
// steps: e.g. [{ kind: 'neon-branch-create', … }] or per-table namespace snapshot stepsProvision + tear down
provisionBranch / teardownBranch run a plan through an injected
BranchExecutor (the dialect/Neon adapter). Injected so the lifecycle is
unit-testable with a recording executor — no live DB:
import { provisionBranch, teardownBranch } from '@voltro/database'
const result = await provisionBranch(
{ appSlug: 'acme', prNumber: 128, seed: 'copy', tableNames, dbUrl: connectionString },
executor,
)
// result: { branchId, mechanism, steps, … } — for 'neon-cow' also the
// branch's own connection string to bind the preview store to.
// …run the preview against result, then:
await teardownBranch(result.branchId, result.mechanism, executor)admitBranch gates whether a branch may be served (e.g. it exists + is ready)
so a request never binds to a half-provisioned branch.
Mechanisms
| mechanism | when | how |
|---|---|---|
namespace-snapshot |
any dialect (default) | snapshot the tenant's namespace → restore into a new branch namespace |
neon-branch |
Postgres on Neon | a copy-on-write Neon branch (instant, storage-shared) |
The mechanism is resolved from the connection, so the same planBranch call
produces the right steps for the deployment's database — dialect-agnostic by
default, with the Neon fast-path when it's available. No code path is locked to a
vendor.
Branching copies data, not the schema definition — apply migrations to a branch the same way you would to any store. Teardown is explicit; a branch left bound holds its namespace/Neon branch until torn down.