- Published on
Database Branching — Git-Like Workflows for Your Schema
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Git changed software development. Before git, merging code was painful and error-prone. After git, branches are cheap and merging is routine.
Database branching brings the same paradigm to databases. Every PR gets its own database. Test migrations safely. Review schema changes before merge. Never worry about shared test databases again.
This capability, unthinkable a decade ago, is now standard (Neon, PlanetScale). It''s transforming how teams develop.
This post covers database branching strategies, workflows, and automation.
- What Is Database Branching?
- Neon Branch-Per-PR Workflow
- PlanetScale''s Branching Model
- Testing Migrations Safely in Branches
- Schema Diff Before Merge
- Reverting a Bad Migration
- Automating Branch Creation in CI
- Branch Cleanup Automation
- Seeding Test Data in Branches
- Comparing Database Branching Tools
- When Branching Is Overkill
- Real-World Example: Feature Branch Workflow
- Checklist
- Conclusion
What Is Database Branching?
Database branching creates a copy of the database for isolated testing.
Traditional workflow:
- Develop feature (local)
- Push to staging (shared)
- Run migrations on staging (affects all developers)
- Test (pray others aren''t breaking it)
- Deploy to prod
New workflow (with branching):
- Develop feature (local)
- Create database branch (copy of prod schema/data)
- Push feature branch (uses database branch)
- Automated testing on PR (with isolated database)
- Merge PR (database branch deleted, automatic)
No shared staging database. No "sorry, I broke staging." No "we can''t test, schema is changing."
Neon Branch-Per-PR Workflow
Neon makes branching effortless. Every PR gets a database branch automatically.
Setup in GitHub Actions:
name: Create Database Branch
on:
pull_request:
types: [opened]
jobs:
create-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create Neon branch
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
run: |
BRANCH_ID=$(curl -X POST https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches \
-H "Authorization: Bearer $NEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"branch": {
"parent_id": "main",
"name": "pr-${{ github.event.pull_request.number }}"
}
}' | jq -r '.branch.id')
echo "BRANCH_ID=$BRANCH_ID" >> $GITHUB_OUTPUT
- name: Comment on PR
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Database branch created: `pr-${{ github.event.pull_request.number }}`\n\nPreview: https://console.neon.tech/...'
})
cleanup:
runs-on: ubuntu-latest
needs: create-branch
steps:
- name: Delete branch on close
if: github.event.action == 'closed'
run: |
curl -X DELETE https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches/$BRANCH_ID \
-H "Authorization: Bearer $NEON_API_KEY"
Now, preview deployments automatically use the PR''s database branch. No setup, no manual steps.
PlanetScale''s Branching Model
PlanetScale (MySQL) also supports branching. Different from Neon but equally powerful.
# Create a branch from main
pscale branch create my-db dev-feature-123
# List branches
pscale branch list my-db
# Apply migrations to branch
pscale deploy-request create my-db dev-feature-123
# Review and merge
pscale deploy-request review my-db <id>
pscale deploy-request deploy my-db <id>
PlanetScale branches are lighter-weight (don''t copy all data). Schema changes are queued in "deploy requests." Review before deploying.
Workflow:
- Create branch
- Write migration
- Create deploy request
- Review (schema diff visible)
- Deploy (automatic, zero-downtime)
Safer than Neon (explicit approval), but slower (queued deployments).
Testing Migrations Safely in Branches
Migrations are risky. A slow migration locks tables. A bad schema change breaks queries.
With branches, test migrations for free:
// migration.sql
ALTER TABLE users ADD COLUMN email_normalized VARCHAR(255);
CREATE INDEX idx_users_email_normalized ON users(email_normalized);
-- Test: run this migration on a branch
-- Verify it completes in <1 second (no locks)
-- Verify no queries fail
-- Then deploy to production
In CI, run tests against the branch:
name: Test Migrations
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run migrations on branch
env:
DATABASE_URL: ${{ secrets.NEON_BRANCH_URL }}
run: npx drizzle-kit migrate:postgres
- name: Run test suite
env:
DATABASE_URL: ${{ secrets.NEON_BRANCH_URL }}
run: npm test
- name: Benchmark query performance
env:
DATABASE_URL: ${{ secrets.NEON_BRANCH_URL }}
run: npm run benchmark
If tests fail, the branch is rolled back automatically. Production is never touched.
Schema Diff Before Merge
With branches, you can show the exact schema changes before merging.
Neon API:
curl -X GET https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches/$BRANCH_ID/operations \
-H "Authorization: Bearer $NEON_API_KEY" | jq '.operations[] | select(.type == "migrations")'
PlanetScale deploy request shows diff:
pscale deploy-request view my-db <id> --json
Output:
{
"from_schema": "CREATE TABLE users (...)",
"to_schema": "CREATE TABLE users (...), CREATE TABLE posts (...)",
"statements": [
"CREATE TABLE posts (id INT PRIMARY KEY, user_id INT REFERENCES users(id))"
]
}
Reviewers see exactly what changed. No surprises.
Reverting a Bad Migration
With branches, reverting is trivial.
If a production migration causes issues:
# Neon: revert branch to previous state
neon branches reset --name main --to-commit <commit-hash>
# Or, create a reverse migration
ALTER TABLE users DROP COLUMN email_normalized;
DROP INDEX idx_users_email_normalized;
Deploy the reverse migration. Done in seconds, no downtime.
Without branches, reverting is stressful and time-consuming.
Automating Branch Creation in CI
Completely automate the workflow:
name: Database Workflow
on: [pull_request, push]
jobs:
create-or-cleanup:
runs-on: ubuntu-latest
outputs:
branch_url: ${{ steps.create.outputs.branch_url }}
steps:
- uses: actions/checkout@v3
- name: Create branch (PR opened)
if: github.event_name == 'pull_request' && github.event.action == 'opened'
id: create
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
run: |
RESPONSE=$(curl -X POST https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches \
-H "Authorization: Bearer $NEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"branch": {
"parent_id": "main",
"name": "pr-${{ github.event.pull_request.number }}"
}
}')
BRANCH_URL=$(echo $RESPONSE | jq -r '.branch.connection_uri')
echo "branch_url=$BRANCH_URL" >> $GITHUB_OUTPUT
- name: Delete branch (PR closed)
if: github.event_name == 'pull_request' && github.event.action == 'closed'
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
run: |
curl -X DELETE https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches/pr-${{ github.event.pull_request.number }} \
-H "Authorization: Bearer $NEON_API_KEY"
test:
needs: create-or-cleanup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run migrations
env:
DATABASE_URL: ${{ needs.create-or-cleanup.outputs.branch_url }}
run: npx drizzle-kit migrate:postgres
- name: Run tests
env:
DATABASE_URL: ${{ needs.create-or-cleanup.outputs.branch_url }}
run: npm test
- name: Deploy to Vercel
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: vercel deploy --prod --token=$VERCEL_TOKEN
Every PR:
- Creates a database branch (automatic)
- Runs migrations (automatic)
- Runs tests against branch (automatic)
- Deploys preview to Vercel (automatic)
- Deletes branch on close (automatic)
No manual steps. No human error.
Branch Cleanup Automation
Branches cost money. Forgotten branches add up.
Cleanup policy:
# Delete branches older than 7 days
pscale branch list my-db --format json | \
jq '.[] | select(.created_at < now - 7 days) | .name' | \
xargs -I {} pscale branch delete my-db {}
Or, configure automatic cleanup:
name: Cleanup Old Branches
on:
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Delete branches > 7 days old
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
run: |
BRANCHES=$(curl -X GET https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches \
-H "Authorization: Bearer $NEON_API_KEY" | jq '.branches[]')
echo "$BRANCHES" | jq -r '.name' | while read branch; do
CREATED=$(echo "$BRANCHES" | jq -r --arg b "$branch" '.[] | select(.name == $b) | .created_at')
AGE_DAYS=$(( ($(date +%s) - $(date -d "$CREATED" +%s)) / 86400 ))
if [ $AGE_DAYS -gt 7 ]; then
echo "Deleting branch: $branch (age: ${AGE_DAYS} days)"
curl -X DELETE https://api.neon.tech/v1/projects/$NEON_PROJECT_ID/branches/$branch \
-H "Authorization: Bearer $NEON_API_KEY"
fi
done
Automatic cleanup prevents surprise bills.
Seeding Test Data in Branches
Each branch starts as a copy of production. For testing, populate with test data:
// scripts/seed-branch.ts
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
export async function seedBranch() {
// Only seed if branch (check for marker)
const isBranch = await sql`
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = '_branch_marker'
);
`;
if (!isBranch[0].exists) {
console.log('Not a branch, skipping seed');
return;
}
// Insert test users
await sql`
INSERT INTO users (id, email, name) VALUES
(gen_random_uuid(), 'test@example.com', 'Test User'),
(gen_random_uuid(), 'admin@example.com', 'Admin')
`;
// Insert test posts
await sql`
INSERT INTO posts (id, user_id, title) VALUES
(1, (SELECT id FROM users WHERE email = 'test@example.com'), 'Test Post')
`;
console.log('Seeded branch with test data');
}
Run in CI after creating branch:
- name: Seed branch
env:
DATABASE_URL: ${{ secrets.NEON_BRANCH_URL }}
run: npx ts-node scripts/seed-branch.ts
Now your preview environment has realistic test data without affecting production.
Comparing Database Branching Tools
| Tool | Model | Cost | ZDD | API |
|---|---|---|---|---|
| Neon | Full DB copy | Per branch | ✓ | REST |
| PlanetScale | Schema only | Per deploy request | ✓ | CLI |
| Planetfall | Lightweight | Per branch | ✓ | Unknown |
| Supabase | N/A | N/A | N/A | N/A |
Neon: most flexible, lightweight cost. Good for small branches.
PlanetScale: explicit approval workflow, zero-downtime deploys, MySQL-only.
Neither is perfect. Neon is easier to automate. PlanetScale has safer workflows.
When Branching Is Overkill
Branching adds complexity. For small teams or simple schemas, it might not be worth it.
Don''t use branching if:
- Small team (1–3 engineers)
- Simple schema (<10 tables)
- Rare migrations (monthly)
- Low risk tolerance (prefer simplicity)
In these cases, a single shared staging database is acceptable. Coordinate migrations carefully.
Use branching if:
- Growing team
- Complex schema
- Frequent migrations
- Multiple teams (avoid merge conflicts)
Real-World Example: Feature Branch Workflow
Engineer pushes feature branch
↓
GitHub Actions triggered
↓
Neon branch created (pr-123)
↓
Migrations run (drizzle-kit migrate)
↓
Tests run (npm test)
↓
Benchmarks run (query perf checked)
↓
Preview deployed (Vercel with isolated DB)
↓
QA tests preview
↓
PR approved and merged
↓
Neon branch deleted (automatic)
↓
Production deployment (uses main branch)
↓
Success!
Entire flow is automated. Zero manual database steps. Schema changes are proven before production.
This is the future of database development.
Checklist
- Choose branching tool (Neon or PlanetScale)
- Set up GitHub Actions for auto-branching
- Create test suite that runs on branches
- Set up schema diff visibility in PRs
- Automate branch cleanup (weekly)
- Seed test data in branches
- Document branch workflow for team
- Test migration rollback
- Monitor branch costs
- Integrate with Vercel/deployment system
Conclusion
Database branching is no longer experimental. It''s the best practice for database development.
Neon and PlanetScale have made it accessible. Automated CI integration means zero manual overhead.
The benefits are substantial: safer migrations, faster feedback, no shared database contention, and confidence in schema changes.
If you''re still using shared staging databases, branching is a step up. Adopt it. Your team will thank you.