Database setupNeon + Prisma Cheat Sheet

Neon + Prisma Cheat Sheet

Prisma tracks applied migrations in the prismamigrations table inside each branch. prisma migrate deploy only applies migrations that have no row in that table.

Neon + Prisma Cheat Sheet

Quick reference for how the database stack fits together and what to run when things break. Start here before digging into longer docs.


1. How the Stack Fits Together

Two Connection Paths — Why?

ConsumerConnectionReason
Cloudflare Worker (production)env.HYPERDRIVE.connectionString → NeonHyperdrive is Cloudflare-edge-only: warm TCP pool, sub-ms overhead, caching
GitHub Actions (CI migrations)DIRECT_DATABASE_URL → Neon directlyHyperdrive is unreachable outside Cloudflare's network
Local dev — Dockerpostgresql://adblock:localdev@localhost:5432/adblock_devOffline, no credentials needed
Local dev — Neon branchDIRECT_DATABASE_URL → Neon directlySame path as CI

Hyperdrive is not a general-purpose connection pooler. It is a Cloudflare edge service that only runs inside a Worker. prisma migrate deploy runs on a standard Linux VM in GitHub Actions and cannot reach it. This is by design — not a bug.


2. Key Concepts

Neon Terminology

TermWhat it isThis project
ProjectTop-level Neon containerbloqr-db-aws (ID: steep-rice-35228530)
BranchGit-like copy-on-write clone of a projectproduction (Default), main, pr-N
DatabasePostgreSQL database inside a branchadblock-compiler
RolePostgreSQL user/roleneondb_owner (Neon default)
EndpointCompute attached to a branchep-summer-pine-b73vvmyj

Connection String Anatomy

postgresql://neondb_owner:PASSWORD@ep-xxx-pooler.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require
└─────────┘ └────────────────────┘ └─────────────────────────────────────────────────────┘ └──────────────┘
  scheme      role:password          host (add -pooler for app; omit for migrations/psql)    database name
  • Pooled endpoint (app / Hyperdrive): hostname contains -pooler
  • Direct endpoint (migrations / psql): hostname has no -pooler suffix

Prisma Migration State

Prisma tracks applied migrations in the _prisma_migrations table inside each branch. prisma migrate deploy only applies migrations that have no row in that table.

_prisma_migrations
migration_namestarted_atfinished_atrolled_back_atstatus
20260322030000_init2026-03-22 03:00:002026-03-22 03:00:01NULLapplied ✅
20260322092000_add_two_factor_table2026-03-22 09:20:00NULLNULLFAILED ❌

finished_at IS NULL and rolled_back_at IS NULL ⇒ migration is considered failed and causes P3009.


3. Daily Commands

Check migration status

DIRECT_DATABASE_URL="<connection-string>" npx prisma migrate status

Apply pending migrations

DATABASE_URL="<pooler-url>" DIRECT_DATABASE_URL="<direct-url>" npx prisma migrate deploy

Deploy pending migrations to the production Neon branch

Use this when the production branch is behind local. The production Neon branch has its own _prisma_migrations table — migrations applied to dev/staging branches are not automatically promoted to production. You must run migrate deploy explicitly against the production direct connection string.

# Obtain the production direct connection string from:
#   https://console.neon.tech → project bloqr-db-aws → branch: production
#   → Connection Details → Direct (not pooled)
DIRECT_DATABASE_URL="postgresql://neondb_owner:<password>@ep-xxx.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require" \
  deno task db:migrate:deploy

Or using npx prisma directly (equivalent; useful without Deno):

DIRECT_DATABASE_URL="postgresql://neondb_owner:<password>@ep-xxx.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require" \
  DATABASE_URL="postgresql://neondb_owner:<password>@ep-xxx-pooler.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require" \
  npx prisma migrate deploy

⚠️ Always use the direct (non-pooled) endpoint for DIRECT_DATABASE_URL.
The pooler endpoint (hostname contains -pooler) is for application connections through Hyperdrive. Prisma requires a direct connection for DDL operations.

Verify afterwards:

DIRECT_DATABASE_URL="postgresql://neondb_owner:<password>@ep-xxx.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require" \
  npx prisma migrate status
# Expected: "All migrations have been applied."

Open Prisma Studio (visual DB browser)

DIRECT_DATABASE_URL="<direct-neon-url>" npx prisma studio

Connect via psql

psql "postgresql://neondb_owner:PASSWORD@ep-xxx.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require"

Inspect migration history

-- Run inside psql or Prisma Studio
SELECT migration_name, started_at, finished_at, rolled_back_at
FROM _prisma_migrations
ORDER BY started_at;

4. Neon Branch Commands

All examples use the neonctl CLI. Install with npm i -g neonctl, then authenticate:

neonctl auth

List all branches

neonctl branches list --project-id steep-rice-35228530

Get connection string for a branch

neonctl connection-string pr-1278 \
  --project-id steep-rice-35228530 \
  --database-name bloqr-backend \
  --role-name neondb_owner

Delete a stuck PR branch manually

neonctl branches delete pr-1278 --project-id steep-rice-35228530

Restore a branch to its parent state (via Neon REST API)

# Get the branch ID first
BRANCH_ID=$(neonctl branches get pr-1278 --project-id steep-rice-35228530 -o json | jq -r '.id')

# Restore to parent tip (instant — copy-on-write)
curl -sf -X POST \
  "https://console.neon.tech/api/v2/projects/steep-rice-35228530/branches/${BRANCH_ID}/restore" \
  -H "Authorization: Bearer ${NEON_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"source_type":"parent"}'

5. Error Reference

P3009 — Failed migration blocking deploys

Error: P3009
migrate found failed migrations in the target database, new migrations will not be applied.
The `20260322092000_add_two_factor_table` migration started at 2026-03-22 19:14:21 UTC failed

What happened: A previous CI run failed mid-migration. Prisma wrote a row to _prisma_migrations with started_at set but finished_at NULL. Every subsequent prisma migrate deploy sees the failed row and refuses to proceed.

Important — prisma migrate status exit codes:
  • Exit 0: schema is in sync, nothing pending
  • Exit 1: migrations are pending or failed — both cases exit 1

A non-zero exit code alone does not mean P3009. On every fresh PR branch there will always be pending migrations that haven't been applied yet, and migrate status exits 1 for those too. Only look for the literal string P3009 in the output.

Automatic fix: The neon-branch-create.yml workflow detects P3009 by grepping for the literal string P3009 in the migrate status output (never by exit code alone) and restores the PR branch to its production parent before re-running migrations. Push a new commit or click Re-run jobs to trigger recovery.

Manual fix (Option A — resolve single migration):
DIRECT_DATABASE_URL="<branch-url>" \
  npx prisma migrate resolve --rolled-back 20260322092000_add_two_factor_table
# Then re-run migrations
DIRECT_DATABASE_URL="<branch-url>" DATABASE_URL="<pooler-url>" npx prisma migrate deploy
Manual fix (Option B — restore entire PR branch):
# Safest for PR branches: wipes schema + migration history, re-forks from production
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"}'

"relation already exists"

ERROR: relation "two_factor" already exists

What happened: A table was created by one migration, and a second migration tries to create the same table (e.g. duplicate migrations, or a migration file was renamed/re-added after the table already exists in the branch).

Fix:
# 1. Drop the orphaned table
psql "$DIRECT_DATABASE_URL" -c 'DROP TABLE IF EXISTS "two_factor" CASCADE;'

# 2. Remove the orphaned migration record (if the file no longer exists on disk)
psql "$DIRECT_DATABASE_URL" \
  -c "DELETE FROM _prisma_migrations WHERE migration_name = 'old_migration_name';"

# 3. Re-run migrations
npx prisma migrate deploy

Migration drift — applied but not on disk

Drift detected: Your database schema is not in sync with your migration history.
The following migration(s) are applied to the database but missing from the local migrations directory:
• 20260322030001_add_two_factor

What happened: A migration was applied to the branch, then the .sql file was deleted from the repo. Prisma sees the row in _prisma_migrations but finds no file.

Fix for PR branches (simplest): Restore the branch to parent (see P3009 Option B above).

Fix without branch restore:
# Delete the orphaned record — lets Prisma treat the schema as the baseline
psql "$DIRECT_DATABASE_URL" \
  -c "DELETE FROM _prisma_migrations WHERE migration_name = '20260322030001_add_two_factor';"

NEON_DATABASE_URL not set / can't parse

::error::Could not parse database name or role from NEON_DATABASE_URL.

Fix: Add the secret in GitHub → repository Settings → Secrets and variables → Actions → New repository secret:

Secret nameValue format
NEON_DATABASE_URLpostgresql://neondb_owner:PASSWORD@ep-xxx.c-13.us-east-1.aws.neon.tech/bloqr-backend?sslmode=require
NEON_API_KEYNeon API token from console.neon.tech → Account → API Keys
NEON_PROJECT_IDsteep-rice-35228530

Note: Use the direct (non-pooled) endpoint in NEON_DATABASE_URL — the URL must not contain -pooler in the hostname. Prisma requires a direct connection for schema operations.


6. GitHub Actions Secrets Reference

SecretRequired byValue
NEON_API_KEYneon-branch-create.yml, neon-branch-cleanup.ymlNeon API token
NEON_PROJECT_IDboth Neon workflowssteep-rice-35228530
NEON_DATABASE_URLneon-branch-create.ymlDirect connection string (non-pooled) pointing to bloqr-backend database
DIRECT_DATABASE_URLdb-migrate.ymlSame direct connection string — used by production migration jobs

The workflow derives the database name (bloqr-backend) and role (neondb_owner) from NEON_DATABASE_URL at runtime. If the database is ever renamed, update only that secret — nothing else needs to change.


DocumentWhat it covers
neon-setup.mdFull Neon project setup, Hyperdrive config, PrismaClient factory
neon-branching.mdPR branch lifecycle, workflow details, troubleshooting
local-dev.mdLocal Docker PostgreSQL setup
migration-checklist.mdD1 → Neon one-time migration checklist
prisma-deno-compatibility.mdWhy deno task db:generate must be used instead of npx prisma generate