Database Branching
A branch is a fully-isolated clone of a project’s database that you develop or test against, then throw away. It is AnvilBase’s take on the Neon branching workflow: spin up a copy of production data (or just its schema) for a feature branch, a migration dry-run, or a CI job, run whatever you want against it, and delete it when you’re done — the parent is never affected.
A branch is a full tenant. It gets its own database, own API keys (anon + service_role), own JWT secret, and own isolation boundary — it is indistinguishable from a top-level project to every part of the platform (auth, RLS, the connection pooler, resource limits). It is not a view, a schema, or a row-level slice of the parent.
What v1 does (and doesn’t)
AnvilBase branching v1 is dump/clone-based, not copy-on-write:
from: "now"— clones the parent’s current state. This is the only source available in v1.schema_only— clone the DDL only (tables, functions, policies, …) with zero rows. Useful for a clean structural copy or CI schema checks.- Timing — cloning uses an in-engine
CREATE DATABASE … TEMPLATEfast path when possible (near-instant), and falls back topg_dump | pg_restorewhen the parent has active connections. Expect ~30–60s for a data branch of a non-trivial database on the dump path. This is honest: there is no copy-on-write in v1, so a branch physically copies the parent’s data. - Disk headroom — because a branch is a full copy, a branch of a very
large parent needs a second copy’s worth of database storage, and the
pg_dump | pg_restorefallback also writes a transient dump to the control-plane’s/tmp(deleted as soon as the restore finishes). Ensure database storage and/tmphave headroom before branching a large database. (A v1 follow-up will add an explicit size/timeout cap.)
Deferred to v2:
from: "<restore_point>"(branching from a point in time via PITR) — the API rejects anyfromother than"now"in v1 with a clear400.- Merge / deploy-request flow (promoting a branch’s schema changes back to the parent, à la Neon “deploy requests”) — there is no merge in v1. A branch is a throwaway copy; apply schema changes to the parent through your normal migration workflow.
Isolation guarantees
A branch is a tenant-isolation security boundary — as isolated as a top-level project:
- Distinct credentials. The branch’s anon key, service_role key, and JWT secret are freshly minted and distinct from the parent’s. After cloning, the platform re-keys the branch: the cloned JWT secret is overwritten with the branch’s own, so the branch’s auth verifies only branch-signed tokens.
- Cross-tenant access is denied. The parent’s service_role key cannot read or write the branch’s data plane (and the branch’s key cannot reach the parent) — every request is checked against the credential’s own tenant.
- Database-level lockdown. The branch’s connection-pooler role is its own
(
pooler_<branch_id>), and the parent’s pooler credential’sCONNECTgrant is revoked from the branch database, so a parent pooled connection can’t reach it. - Write isolation. Writing to a branch never changes the parent, and writing to the parent never changes an existing branch — they are separate databases.
Create a branch
API
POST /api/v1/projects/{id}/branchesAuthorization: Bearer <admin-or-service-token>Content-Type: application/json
{ "name": "dev", "from": "now", "schema_only": false }The response returns the branch’s own keys once — save them, they are never shown again:
{ "branch": { "branch_project_id": "…", "slug": "dev", "db_name": "platform_…", "status": "ready", "schema_only": false, "from_kind": "now", "clone_method": "template" }, "anon_key": "anvilbase_anon_…", "service_role_key": "anvilbase_service_role_…", "jwt_secret": "…"}Creating a branch requires an admin-scoped management credential.
CLI
# Full-data branch (clones the parent's current rows)anvilbase branch create --project <project-id> dev
# Schema-only branch (DDL, zero rows)anvilbase branch create --project <project-id> ci --schema-onlyThe CLI prints the branch’s anon key, service_role key, and JWT secret on creation — these are distinct from the parent’s and shown only once.
List branches
anvilbase branch list --project <project-id>GET /api/v1/projects/{id}/branchesDelete a branch
Deleting a branch drops its database, its API keys, its pooler tenant, and its storage bucket. The parent is never touched.
anvilbase branch delete --project <project-id> devDELETE /api/v1/projects/{id}/branches/{slug}A branch is addressed by its slug under its parent project, so a delete can
only ever target a branch of that parent — never the parent itself, never another
tenant’s branch.
Connecting to a branch
Because a branch is a full tenant, you use it exactly like a project: point the SDK at the branch’s project ID and use the branch’s keys.
import { createClient } from "@supabase/supabase-js";
// The branch's own project id + anon key (from `branch create`)const branch = createClient( "https://<host>/v1/<branch-project-id>", "<branch-anon-key>",);The branch’s pooled and direct connection strings are available the same way as any project — see Connection Pooling.
When to use a branch
- Feature development — branch production data, build against a realistic copy, throw it away.
- Migration dry-runs — branch, apply a migration, verify, delete; if it goes wrong the parent is untouched.
- CI / ephemeral test environments — a
--schema-onlybranch gives a clean structural copy with no data to manage.
Because v1 physically copies data, branches are best for short-lived, develop-and-discard workflows rather than long-running parallel environments.