Skip to content

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 … TEMPLATE fast path when possible (near-instant), and falls back to pg_dump | pg_restore when 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_restore fallback also writes a transient dump to the control-plane’s /tmp (deleted as soon as the restore finishes). Ensure database storage and /tmp have 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 any from other than "now" in v1 with a clear 400.
  • 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’s CONNECT grant 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}/branches
Authorization: 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

Terminal window
# 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-only

The 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

Terminal window
anvilbase branch list --project <project-id>
GET /api/v1/projects/{id}/branches

Delete a branch

Deleting a branch drops its database, its API keys, its pooler tenant, and its storage bucket. The parent is never touched.

Terminal window
anvilbase branch delete --project <project-id> dev
DELETE /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-only branch 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.