Prisma Migrate Shadow DB Errors — Quick Fixes
prisma migrate dev fails with a shadow database error: permission denied to create database, or P3014-style reset failures. The main DATABASE_URL works for the app. Staging migrate deploy in CI is green. Only interactive migrate on a laptop fails. Prisma is not being mystical — migrate dev needs a disposable database to diff schema against history, and your role cannot create or reset it.
What the shadow database is for
On migrate dev, Prisma:
- Uses (or creates) a temporary shadow database.
- Applies existing migration SQL there from scratch.
- Compares that result to your current Prisma schema.
- Generates the next migration from the diff.
It is a scratch playground. Production prisma migrate deploy applies already-generated SQL and does not need a shadow DB. That split explains “CI works, my machine doesn’t.”
Cannot create database
permission denied to create database
Your user can CONNECT to mydb but lacks CREATEDB.
Grant on local Docker (dev only):
ALTER USER app_user CREATEDB;
Or point at a pre-created shadow database:
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
SHADOW_DATABASE_URL="postgresql://user:pass@localhost:5432/mydb_shadow"
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
Create mydb_shadow once with a privileged role. Prisma will wipe and rebuild its contents; never point shadow at a database with data you care about.
Pooler URLs break migrate
PgBouncer in transaction mode and some serverless poolers break migration and shadow workflows (prepared statements, CREATE DATABASE, advisory locks). Use a direct connection for migrate:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // pooled — app runtime
directUrl = env("DIRECT_URL") // direct — migrate
}
Keep the pooler for the running app. Run migrate dev / migrate deploy against directUrl. Mixing them is how you get opaque timeouts that look like shadow bugs.
Hosted Postgres that forbids CREATE DATABASE
Neon, Supabase, RDS setups with locked-down users, and shared hosts often disallow creating sibling databases.
- Set
shadowDatabaseUrlto an empty DB you are allowed to reset. - Or generate migrations against local Docker Postgres, then
migrate deployto hosted. - Never run
migrate devagainst production credentials — shadow resets are destructive by design.
Drift and reset pressure
If someone changed staging by hand (db push, SQL console), history and reality disagree. migrate dev may offer a reset. On shared environments prefer:
- Create migrations in isolation (local / personal DB).
- Apply with
migrate deployto shared staging. - Avoid casual resets that wipe teammate data.
Docker recipe that avoids cloud permission fights
services:
db:
image: postgres:16
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: app
ports: ["5432:5432"]
DATABASE_URL=postgresql://prisma:prisma@localhost:5432/app
A local superuser-equivalent can create shadow DBs without ticket-driven grants.
Command cheat sheet
| Command | Needs shadow? | Typical place |
|---|---|---|
prisma migrate dev | Yes | Developer laptop |
prisma migrate deploy | No | CI / prod |
prisma db push | No | Prototyping only — no migration history |
CI should call deploy, not dev.
Recovery order
- Confirm the failure is only on
migrate dev. - Switch migrate to
directUrlif you use a pooler. - Set
shadowDatabaseUrlto a disposable DB, or grantCREATEDBlocally. - Generate SQL locally; deploy elsewhere.
- If drift is severe, baseline carefully — do not reset production to “make Prisma happy.”
Shadow DB errors are permission and topology problems. Give Prisma a playground (or local Docker), keep poolers off the migrate path, and the scary migrate message becomes an env-line fix instead of a day lost to schema folklore.