Skip to content

Database Branch Management

When to use: Before any schema change, migration, destructive query, or risky data work against a managed Postgres database — validate it on a short-lived branch first, never directly on staging or production.

Instructions

Modern managed Postgres (Neon, Databricks Lakebase) supports copy-on-write branches: a near-instant, isolated copy of the database you can mutate freely and throw away. Treat every schema change, migration, or destructive query as something that must be proven on a branch before it reaches staging or production.

This skill covers two providers. Pick the section that matches your stack:

  • Neonneonctl CLI workflow (concrete, end-to-end below).
  • Lakebase (Databricks managed Postgres) — equivalent workflow via the Databricks CLI/API.

The governance principles are shared and provider-agnostic; see database-change-control for the schema-first change-control rules this workflow operates inside.

Core rules (both providers)

  • Never run a migration or destructive SQL (DROP, TRUNCATE, ALTER ... DROP COLUMN) against staging/production before validating it on a branch.
  • Always set an expiry/TTL on temporary branches (default 48 hours) so exploratory copies clean themselves up. Skip expiry only for long-lived branches tied to a specific feature branch in CI.
  • Name branches for what they do: dev/<name>-explore, schema/add-orders-table, migration/0005-add-indexes.
  • Production migrations are CI's job. Validate on a branch locally, commit the migration files, open a PR. The deploy pipeline applies migrations to production on merge — you do not run them against production by hand.

Neon workflow

Prerequisites

neonctl --version            # check the CLI is installed
npm install -g neonctl       # install if missing — https://neon.tech/docs/reference/cli-install
neonctl auth                 # authenticate (first time only)
neonctl projects list        # find your project id
neonctl set-context --project-id <project-id>   # avoid --project-id on every call

1. Create a branch (with a 48-hour TTL)

# Linux/GNU (WSL2)
neonctl branches create --name <name> --expires-at "$(date -u -d '+48 hours' +%Y-%m-%dT%H:%M:%SZ)"

# macOS/BSD
neonctl branches create --name <name> --expires-at "$(date -u -v+48H +%Y-%m-%dT%H:%M:%SZ)"

For a longer-lived branch tied to a GitHub feature branch, omit --expires-at and delete it manually when the branch merges:

neonctl branches create --name <descriptive-name>

2. Get the branch connection string

neonctl connection-string <branch-name>

Returns a postgresql://... URI. Use it in place of DATABASE_URL for every subsequent operation against the branch.

3. Apply changes on the branch

Prefer your project's branch-safe wrapper scripts if it has them — they validate the target is a temporary branch (not main/staging/production) and wire up the connection string. For example, a Drizzle-based project might expose:

npm run db:branch:migrate -- <branch-name>   # run a generated migration
npm run db:branch:push    -- <branch-name>   # push schema for fast iteration

If your project blocks raw migration tools (e.g. drizzle-kit push/migrate) via deny rules, the wrapper scripts are the only sanctioned path — use them.

4. Validate

DATABASE_URL="<branch-connection-string>" npm test
neonctl branches schema-diff <branch-name> --compared-to main   # optional

Do not proceed until validation passes.

5. Apply to the real database

Commit the migration files, push to a feature branch, open a PR. CI validates on a fresh temp branch and applies to production on merge. Do not run migrations against staging/production locally.

6. Clean up

neonctl branches delete <branch-name>

If the branch had --expires-at, cleanup is automatic and this step is optional.

Neon quick reference

Task Command
List branches neonctl branches list
Create branch neonctl branches create --name <name>
Get connection string neonctl connection-string <branch-name>
Compare schemas neonctl branches schema-diff <branch> --compared-to main
Reset branch to parent neonctl branches reset <branch-name> --parent
Delete branch neonctl branches delete <branch-name>

Lakebase workflow (Databricks managed Postgres)

Lakebase exposes the same branching idea through Databricks database instances: you create a child instance from a parent at a point in time, work against it, then delete it. The Databricks CLI surface is evolving — run databricks database --help and confirm exact flags against current docs (Lakebase docs) before scripting.

The workflow mirrors Neon:

  1. Authenticate with the Databricks CLI (databricks auth login) and select the target workspace profile.
  2. Create a child branch from the parent instance at the current (or a point-in-time) state. A child instance is an isolated copy-on-write Postgres you can mutate freely.
  3. Get the branch connection details for the child instance and use them in place of your production DATABASE_URL.
  4. Apply and validate schema changes / migrations against the child, then run the test suite against it — same gate as Neon step 4.
  5. Promote via CI, not by hand: commit migration files and let the deploy pipeline apply them to the production instance.
  6. Delete the child instance when done; set a TTL/expiry where the API supports it so abandoned branches don't linger and accrue cost.

Because Lakebase child instances consume compute/storage, deleting them promptly matters more than with Neon — treat cleanup as mandatory, not optional.


References


Troubleshooting

Symptom Likely cause Fix
neonctl: command not found CLI not installed npm install -g neonctl
Commands prompt for --project-id every time Context not set neonctl set-context --project-id <id>
Migration tool blocked / permission denied Project deny rules forbid raw drizzle-kit etc. Use the project's branch-safe wrapper scripts instead
Branch still exists after you expected cleanup No --expires-at was set Delete manually (neonctl branches delete <name>); set a TTL next time
databricks database flags rejected Lakebase CLI surface drifted Run databricks database --help and confirm against current docs before scripting
Unexpected Lakebase compute/storage cost Child instances left running Delete child instances promptly; set a TTL where supported