Neon's Database Branching for Fearless Development
Creating database branches for PRs, running migrations safely, restoring from branch history, and integrating with CI.
Neon is a serverless PostgreSQL provider with a killer feature: database branching. Just like Git branches for code, you can branch your database — spin up an isolated copy of your schema and data, run migrations against it, test, then merge or discard. NodWatch uses this for every non-trivial schema change.
Why Branching Matters
The classic problem: you write a migration, run it against your dev database, push to production, and discover the migration is wrong when it's too late. Rolling back a destructive migration (DROP COLUMN, ALTER TYPE) can mean data loss.
With branching:
- Create a branch from
main(production snapshot) - Run your migration against the branch
- Test with real production-like data
- If it works, run the same migration against production
- If it breaks, discard the branch — main is untouched
Setup
Install the Neon CLI:
npm install -g neonctl
neonctl auth # follow the browser flowCreating Branches
# Create a branch from main
neonctl branches create --name feature/add-download-jobs --project-id YOUR_PROJECT_ID
# Get the connection string for the branch
neonctl connection-string feature/add-download-jobs
# Output: postgresql://user:pass@ep-xyz.us-east-2.aws.neon.tech/neondbSet the branch connection string as your dev DATABASE_URL and run migrations:
DATABASE_URL="postgresql://..." npx drizzle-kit migrateResetting a Branch
If something goes wrong, reset to the parent branch state:
neonctl branches reset feature/add-download-jobs --parentThis is instant — Neon stores branch metadata, not data copies. Branches share storage with copy-on-write semantics.
Point-in-Time Restore
Neon keeps a 7-day (paid: 30-day) history. Restore to any point:
# Restore a branch to 2 hours ago
neonctl branches restore feature/add-download-jobs --timestamp "2026-06-09T10:00:00Z"This is a lifesaver for testing "what was the state before the bad migration?"
CI Integration
In GitHub Actions, create a branch per PR, run migrations and tests against it, then delete on merge:
# .github/workflows/pr-db.yml
name: PR Database Setup
on: [pull_request]
jobs:
setup-db:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Neon CLI
run: npm install -g neonctl
- name: Create DB branch
id: db-branch
run: |
BRANCH_NAME="pr-${{ github.event.number }}"
neonctl branches create --name $BRANCH_NAME --project-id ${{ secrets.NEON_PROJECT_ID }}
CONNECTION_STRING=$(neonctl connection-string $BRANCH_NAME)
echo "connection_string=$CONNECTION_STRING" >> $GITHUB_OUTPUT
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
- name: Run migrations
run: npx drizzle-kit migrate
env:
DATABASE_URL: ${{ steps.db-branch.outputs.connection_string }}
- name: Run tests
run: npm test
env:
DATABASE_URL: ${{ steps.db-branch.outputs.connection_string }}
cleanup-db:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.action == 'closed'
steps:
- name: Delete DB branch
run: neonctl branches delete "pr-${{ github.event.number }}" --project-id ${{ secrets.NEON_PROJECT_ID }}
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}Each PR gets an isolated database with the full production schema and recent data. Tests run against a real Postgres instance.
Neon + Drizzle Migration Workflow
My full migration workflow:
# 1. Create a feature branch in Git and Neon simultaneously
git checkout -b feature/user-preferences
neonctl branches create --name feature/user-preferences
# 2. Set .env.local to use the branch connection string
# DATABASE_URL=postgresql://...feature-branch...
# 3. Modify schema
# db/schema.ts: add userPreferences table
# 4. Generate migration
npx drizzle-kit generate
# 5. Apply to branch
npx drizzle-kit migrate
# 6. Test locally
# 7. Merge code to main, apply migration to production
DATABASE_URL=$PRODUCTION_URL npx drizzle-kit migrate
# 8. Delete the branch
neonctl branches delete feature/user-preferencesAutoscaling
Neon scales to zero when idle — great for dev branches that sit unused between coding sessions. NodWatch's dev branch scales from 0 to handling queries in about 500ms cold start. Production stays warm with autoscaling configured between 0.25 and 4 CUs.
The combination of branching + the serverless HTTP driver via Hyperdrive means you get a production-grade Postgres setup with developer ergonomics that actually work.