Database setupNeon Database Branching for PR Workflows

Neon Database Branching for PR Workflows

When a pull request is opened (or updated), a GitHub Actions workflow creates a Neon database branch forked from production. Prisma migrations are applied...

Neon Database Branching for PR Workflows

Phase 0 of the Neon PostgreSQL migration. Every pull request automatically receives an isolated, copy-on-write database branch so that schema changes and integration tests never touch the production dataset.

Overview

When a pull request is opened (or updated), a GitHub Actions workflow creates a Neon database branch forked from production. Prisma migrations are applied to the new branch, and the connection string is posted as a PR comment. When the PR is closed or merged the branch is deleted automatically.

Because Neon branches are copy-on-write, they share storage with the parent until data diverges — making them essentially free to create and fast to spin up.

How It Works

Workflow Files

FileTriggerPurpose
.github/workflows/neon-branch-create.ymlpull_request: [opened, reopened, synchronize]Creates the Neon branch + runs migrations
.github/workflows/neon-branch-cleanup.ymlpull_request: [closed]Deletes the Neon branch

Required GitHub Repository Secrets

The following secrets must be configured in Settings → Secrets and variables → Actions for the repository:

SecretDescriptionWhere to find it
NEON_API_KEYNeon API authentication tokenNeon Console → Account → API Keys
NEON_PROJECT_IDNeon project identifier (e.g. steep-rice-35228530)Neon Console → Project Settings
NEON_DATABASE_URLConnection string for the main branchNeon Console → Connection Details (select the main branch)

Note: The NEON_DATABASE_URL secret stores the connection string for the main branch. The PR workflows dynamically override this with the branch- specific URL returned by the create-branch action — the secret is used as a baseline for other workflows (e.g. scheduled jobs, production deploys).

Branch Naming Convention

All PR branches follow the pattern:

pr-<pull_request_number>

For example, PR #42 creates a Neon branch called pr-42.

Connecting to a PR Branch for Debugging

When a PR branch is created, a comment is posted on the PR with the connection string. You can use this to connect from any PostgreSQL client:

Using psql

# Copy the connection string from the PR comment
psql "postgresql://neondb_owner:****@ep-xyz-123.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require"

Using your local .env

# Override DATABASE_URL in your local .env to point at the PR branch
DATABASE_URL="postgresql://neondb_owner:****@ep-xyz-123.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require"

# Then run your local dev server — it will use the PR's isolated database
pnpm run ui:dev

Using Prisma Studio

DATABASE_URL="<connection-string-from-pr-comment>" npx prisma studio

Architecture Decisions

Why Neon Branching?

  • Isolation — each PR gets its own database; no cross-contamination between feature branches.
  • Production parity — branches fork from production, so they contain the same schema and (optionally) the same data as production.
  • Zero cost at rest — Neon branches use copy-on-write storage; unchanged pages are shared with the parent.
  • Automatic lifecycle — branches are created and destroyed by CI with no manual intervention.

Relationship to Existing db-migrate.yml

The existing db-migrate.yml workflow handles migration validation and deployment to production backends (D1, PostgreSQL). The Neon branching workflows are complementary:

WorkflowScopeWhen
db-migrate.ymlValidate + deploy to production DBsPush to main or PR touching migrations/
neon-branch-create.ymlCreate isolated PR branch + migrateAny PR opened/updated
neon-branch-cleanup.ymlDestroy PR branchAny PR closed

Git Branching Strategy

Your current workflow is correct

The standard git flow — fix/, feature/, or docs/ branches → PR → merge into main → deploy to production — does not need to change. Neon branches are a CI concern only; they live and die with the PR automatically.

How Neon branches map to your environments

Git branchNeon branchPurpose
Any open PRpr-<number> (auto-created)Isolated migration testing for that PR
mainmainStaging / pre-production schema snapshot
production deployproduction (Default)Live production database

Note: The main Neon branch visible in the console is a legacy branch that predates the production branch being set as Default. PR branches fork from production (not main) so they always have production-parity schema. The main Neon branch can be kept as a staging target or deleted once a proper staging environment is in place.

Do you need more Neon branches for multiple environments?

Not yet. The current setup handles two environments:

  • Production — the production Neon branch, connected via Hyperdrive
  • PR preview — ephemeral pr-N branches, auto-created/deleted by CI

If you add a dedicated staging environment later (e.g. a staging Cloudflare Worker deploy), create a persistent staging Neon branch and point your staging DIRECT_DATABASE_URL at it. No changes to the PR workflow would be needed.

Troubleshooting

Branch already exists

If the Neon branch already exists (e.g. from a previous run), the create action will update it in place. No action needed.

Prisma error P3009 — failed migration blocking deploy

Symptom:
Error: P3009
migrate found failed migrations in the target database, new migrations will not be applied.
The `<migration_name>` migration started at <timestamp> failed

Cause: A previous CI run failed mid-migration (e.g. due to a duplicate table, connection drop, or syntax error). Prisma records the migration as "failed" in the _prisma_migrations table and blocks all future deploys until it is resolved.

Automatic fix: The neon-branch-create.yml workflow detects this condition and automatically restores the PR branch to its production parent tip before re-running migrations. Simply push a new commit (or click "Re-run jobs") to trigger a fresh run.

Manual fix (if automatic recovery fails):
# Option 1 — resolve via Prisma CLI (marks the migration as rolled-back)
DIRECT_DATABASE_URL="<branch-connection-string>" \
  npx prisma migrate resolve --rolled-back <migration_name>

# Option 2 — restore the entire branch to its parent state via the Neon API
#             (safest for PR branches — completely fresh schema + migration history)
curl -sf -X POST \
  "https://console.neon.tech/api/v2/projects/${NEON_PROJECT_ID}/branches/${BRANCH_ID}/restore" \
  -H "Authorization: Bearer ${NEON_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"source_type":"parent"}'

Migrations fail on the branch

Check the GitHub Actions log for the Run Prisma migrations step. Common causes:

  1. Migration drift — a migration was applied to the branch but the file was later removed from the repo. Push a new commit; the workflow's P3009 recovery step will restore the branch and re-apply the canonical migration set.
  2. Invalid migration SQL — fix the migration in your PR and push again.

Why can't migrations go through Hyperdrive?

Hyperdrive is a Cloudflare edge service. It is only accessible from code running inside a Cloudflare Worker on Cloudflare's global network. GitHub Actions runners are standard Linux VMs that live entirely outside Cloudflare's network.

ConsumerConnection pathWhy
Cloudflare Worker (production)env.HYPERDRIVE.connectionString → NeonEdge-local pooling, warm connections, sub-ms overhead
GitHub Actions CI (migrations)DIRECT_DATABASE_URL → Neon directlyHyperdrive unreachable outside Workers
Local dev (Docker)postgresql://adblock:localdev@localhost:5432/adblock_devOffline, no credentials
Local dev (Neon branch)DIRECT_DATABASE_URL → Neon directlySame as CI path

This is correct and expected architecture — not a bug.

Branch not deleted after merge

The cleanup workflow runs on pull_request: [closed]. If it fails, you can manually delete the branch from the Neon Console or via the API:

curl -X DELETE \
  "https://console.neon.tech/api/v2/projects/${NEON_PROJECT_ID}/branches/${BRANCH_ID}" \
  -H "Authorization: Bearer ${NEON_API_KEY}"