Prisma Migrate Shadow DB Errors — Quick Fixes

Prisma PostgreSQL database

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:

  1. Uses (or creates) a temporary shadow database.
  2. Applies existing migration SQL there from scratch.
  3. Compares that result to your current Prisma schema.
  4. 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 shadowDatabaseUrl to an empty DB you are allowed to reset.
  • Or generate migrations against local Docker Postgres, then migrate deploy to hosted.
  • Never run migrate dev against 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:

  1. Create migrations in isolation (local / personal DB).
  2. Apply with migrate deploy to shared staging.
  3. 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

CommandNeeds shadow?Typical place
prisma migrate devYesDeveloper laptop
prisma migrate deployNoCI / prod
prisma db pushNoPrototyping only — no migration history

CI should call deploy, not dev.

Recovery order

  1. Confirm the failure is only on migrate dev.
  2. Switch migrate to directUrl if you use a pooler.
  3. Set shadowDatabaseUrl to a disposable DB, or grant CREATEDB locally.
  4. Generate SQL locally; deploy elsewhere.
  5. 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.