Back to Blog

Prisma Migrate Failing on Railway — Shadow Database Permission Denied

Published: July 28, 2026
Prisma Migrate Failing on Railway — Shadow Database Permission Denied

Prisma migrate shadow database permission denied on Railway is the kind of error that reads like a Postgres outage but is actually Prisma quietly trying to do something you never asked for: create an entire second database, use it for a few seconds, then delete it. prisma migrate dev does this on every run to safely check your migration history against reality — and the moment your database role doesn't have permission to create databases, that invisible step fails loudly, with an error message that never mentions the shadow database by that name until you read it carefully.

Short answer: your Postgres role lacks the CREATEDB privilege, which Prisma Migrate needs to spin up its temporary shadow database. Grant the privilege directly if you control the role, or — more reliably on a managed Railway instance — create a second database by hand and point Prisma at it explicitly via shadowDatabaseUrl, so migrate dev never has to create anything itself.

A person's silhouette cast sharply on a brick wall at sunset, representing the shadow database Prisma creates and quietly discards behind the scenes

What Prisma's Shadow Database Actually Does

The exact error usually looks like this:

TEXT
1Error: A migration failed when applied to the shadow database
2Database error:
3Error querying the database: db error: ERROR: permission denied to create database

Prisma's own documentation on the shadow database explains what it's for: prisma migrate dev replays your entire migration history against a fresh, temporary database, then compares the result to your actual development database. That's how Prisma detects drift — someone ran a manual ALTER TABLE outside of a migration file — and how it evaluates whether a new migration would silently drop data, before it ever touches anything real. Creating that temporary database requires the CREATEDB privilege on the Postgres role Prisma connects as, and that's exactly the permission a lot of Railway setups don't grant by default, particularly on roles provisioned for a shared cluster or restored from a backup rather than a fresh dedicated instance.

(If you've been re-running prisma migrate dev a few times hoping it was a transient Railway blip — it isn't. The role either has CREATEDB or it doesn't, and rerunning the same command against the same permission gets you the same error every time.)

Fix 1: Grant CREATEDB Directly, If You Control the Role

If you have access to a role with sufficient privileges on the Railway instance, the direct fix is a single SQL statement:

SQL
1-- Run against your Postgres instance with a sufficiently privileged role
2ALTER USER your_prisma_user CREATEDB;

This works when you're managing the role yourself and Railway's provisioning didn't restrict it in the first place. It doesn't work — and won't be an option at all — on managed providers that don't expose superuser-level access for security reasons, which is common enough on shared or restored Postgres instances that you shouldn't assume this fix is always available before checking. Prisma's own GitHub issue tracker has a long thread of teams hitting exactly this on various cloud providers, which is worth a skim if your specific error text doesn't quite match what's covered here.

A rustic wooden door secured with a heavy chain and padlock, representing a database role denied the permission it needs to create anything

Fix 2: Configure a Manual Shadow Database With shadowDatabaseUrl

When granting CREATEDB isn't possible or isn't something you want on your primary role, the reliable fix is creating a second, dedicated database yourself and telling Prisma to use it instead of trying to create one on the fly:

Bash
1# .env
2DATABASE_URL="postgresql://user:password@railway-host:5432/mydb"
3SHADOW_DATABASE_URL="postgresql://user:password@railway-host:5432/mydb_shadow"
TypeScript
1// prisma.config.ts
2import 'dotenv/config';
3import { defineConfig, env } from 'prisma/config';
4
5export default defineConfig({
6  schema: 'prisma/schema.prisma',
7  datasource: {
8    url: env('DATABASE_URL'),
9    shadowDatabaseUrl: env('SHADOW_DATABASE_URL'),
10  },
11});

mydb_shadow is a real, empty database you create once — on Railway, that's as simple as connecting to the instance and running CREATE DATABASE mydb_shadow; with whatever role does have that permission, even if it's not the same role your application uses day to day. Prisma then uses it directly for every migrate dev run instead of attempting to create and drop one itself.

Detailed architectural blueprints spread across a desk, representing a separate shadow database created deliberately instead of improvised on the fly

One documented trap worth taking seriously: never point shadowDatabaseUrl at the same database as DATABASE_URL. Prisma treats the shadow database as fully disposable and will drop and recreate its contents — doing that against your real data is not a recoverable mistake.

Fix 3: Sidestep It Entirely With migrate deploy in CI

prisma migrate dev is a development-time command, and it's the only one that needs a shadow database at all. prisma migrate deploy — the command meant for CI/CD pipelines and production — applies already-generated migration files directly against the target database and never creates a shadow database in the process. Prisma's schema-prototyping workflow docs cover db push as a further alternative if you're still iterating on the schema shape and don't need generated migration files yet at all. If the permission issue is only surfacing because you're running migrate dev directly against a shared Railway instance rather than a local Postgres container, the more sustainable fix is often architectural: develop and generate migrations against a local database where you control every permission, and reserve migrate deploy for Railway in CI, where the shadow database requirement never applies.

The Opinion Part

Here's the pattern worth naming, because it shows up in almost every "works on my machine, fails on the managed instance" bug in this genre: a tool's default behavior assumes the permission model of a database you fully control, and a managed cloud provider's whole business model is deliberately not giving you that. Prisma's shadow database is a genuinely good idea — replaying history in isolation before touching real data is exactly the kind of safety check that's cheap to run and expensive to skip. Standish's data on this is blunt: skipping validation and shipping straight to production is a big reason ~19% of software projects fail outright (Standish CHAOS). The fix isn't disabling the safety check because the default permissions don't fit your platform — it's giving the check its own dedicated, low-privilege database to run in, which costs one CREATE DATABASE statement and removes the conflict permanently.

Conclusion

If Prisma Migrate is failing on Railway with a shadow database permission error, it's not a broken migration and it's not a Railway incident — it's a Postgres role that doesn't have CREATEDB, which migrate dev needs for a safety check you never see run successfully. Grant the privilege directly if you can, configure a dedicated shadowDatabaseUrl if you can't, and keep migrate deploy doing the actual production work in CI, where this requirement never comes up at all.

If you're also fighting connection pool exhaustion on the same project, our Prisma-on-Vercel connection pool guide covers the singleton and pooler side of keeping Prisma healthy in production, and if PgBouncer enters the picture once you've got migrations sorted, our prepared statement error guide is the natural next stop.

Create the shadow database once, point Prisma at it, and get back to writing migrations instead of debugging the safety net underneath them.

Frequently Asked Questions

Because prisma migrate dev creates a temporary shadow database behind the scenes to detect schema drift and preview migrations safely, and creating a database requires the CREATEDB privilege on the Postgres role. Depending on how your Railway Postgres instance was provisioned — a shared cluster, a restored backup, or a role created with restricted permissions — your application's database user may not have that privilege, and Postgres refuses the request outright.

It's a throwaway database Prisma creates and deletes automatically during prisma migrate dev, used to replay your entire migration history in isolation and compare the result against your actual development database. That comparison is how Prisma detects drift — someone changed the schema outside of a migration — and how it evaluates whether a new migration would cause data loss, before touching your real data.

Connect to your Railway Postgres instance with a role that has sufficient privileges (Railway's default provisioned user usually qualifies) and run ALTER USER your_user CREATEDB; against the database. This is the simplest fix when you control the role directly, but it isn't available on managed providers that don't expose superuser-level access at all.

It's a separate connection string, configured in Prisma's datasource block, pointing at a database created specifically for Prisma's shadow-database use — bypassing the need for your main application role to have CREATEDB at all. You need it whenever granting CREATEDB isn't possible or isn't something you want to do on your primary database role, which is common on cloud providers with restricted permission models.

No — prisma migrate deploy, the command meant for CI/CD and production, applies already-generated migration files directly and does not create a shadow database at all. If shadow database permissions are only a problem for local development against a shared Railway instance, restricting migrate dev to a local Postgres container and reserving migrate deploy for the real Railway database sidesteps the issue entirely.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio