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 —
neonctlCLI 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) againststaging/productionbefore 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:
2. Get the branch connection string¶
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¶
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¶
- Install the Databricks CLI.
- 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 wronghostin~/.databrickscfgshows up as authorization errors against the org. - Confirm list access (
make lakebase-branches, alist-branchesscript, 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:
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¶
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¶
- Source skill (Neon-only):
storefront-platform/.agents/skills/neon-database/SKILL.md - Neon CLI branching guide
- Neon CLI reference — branches
- Databricks Lakebase (OLTP) docs
- database-change-control — the schema-first governance rules this workflow operates inside.
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 |