Skip to content

Database Branch Management

ContributorsMyles Henaghan

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:

  • Neon — neonctl 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 projects expose copy-on-write branches off a permanent parent (commonly named maindb). Everything except that parent should be temporary: schema experiments, migration validation, and data probes run on a branch with a TTL, never on the parent or on a legacy "production" branch name if the platform has moved production to a separate project.

The Databricks CLI surface evolves — run databricks database --help (and any project-specific wrappers in the repo) against current Lakebase docs before scripting.

Topology (typical)

Layer What to expect
Non-production project Default target for developers; one permanent maindb branch
Production project Separate project (not a branch alias); also has maindb
Temporary branches Named feature-<slug> (lowercase, digits, hyphens; length limits apply)
TTL Default a few days; every temporary branch should expire

Deprecated patterns to avoid: treating a branch named production as live production when production has been split to its own project; branching without TTL; writing migrations directly against maindb.

One-time setup

  1. Install the Databricks CLI.
  2. Authenticate to the non-production workspace profile the repo documents (databricks auth login --profile <profile>). Lakebase may live in a different workspace host than SQL warehouses — stale or wrong host in ~/.databrickscfg shows up as authorization errors against the org.
  3. Confirm list access (make lakebase-branches, a list-branches script, or equivalent). Listing does not prove create permission; you need project manage (or equivalent) to create branches.

Prefer the repository's Make targets or ops scripts over ad-hoc CLI when they encode naming, TTL, and parent branch rules.

Create a branch

# Examples — replace with repo wrappers
make lakebase-branch NAME=my-change            # → feature-my-change + TTL
make lakebase-branch NAME=my-change TTL_DAYS=1

A good wrapper always branches from maindb, sets TTL, and prints connection settings to copy into local env (project id, branch id, host, role hints).

Use the branch

Point application and migration tooling at the branch via env vars the repo documents (for example LAKEBASE_PROJECT, LAKEBASE_BRANCH, LAKEBASE_HOST, OAuth or credential-vending mode). Leave production URLs unset.

Run migrations the same way CI would — credential vending avoids long-lived passwords in files:

uv run --env-file .env alembic upgrade head   # example; adapt to stack

Always set branch and host explicitly when the codebase still carries legacy defaults for old project or branch names.

Access checks

Lakebase Postgres access is often decided in three layers: cloud identity groups and project permissions, Lakebase roles, and Postgres grants. When debugging "permission denied", identify which layer failed before granting ad-hoc access. Repositories sometimes ship a read-only access report script for a given branch and role — use it instead of guessing.

Migration runners often use a dedicated group role; personal roles may list branches but lack table privileges.

Clean up

make lakebase-branches
make lakebase-branch-delete NAME=feature-my-change

Wrappers should refuse to delete maindb, deprecated production branch names, or anything outside the feature-* pattern. Treat manual delete plus TTL as mandatory — Lakebase branches accrue cost faster than Neon branches.

Rules for agents (Lakebase)

  • Ask the driver before creating a branch. Propose name and TTL; wait for yes.
  • Never start ad-hoc local Postgres (container, pgserver) to validate a migration when the engagement standard is Lakebase — use a branch. Offline checks (alembic upgrade --sql, unit tests, in-memory SQLite) need no branch.
  • Never write to, migrate, or delete maindb.
  • Report branch id and expiry in the summary so the driver can delete early.

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