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 steps

Provision + 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.