PostgreSQL

Fix: Can't Migrate Schema Using Prisma With Supabase

The message comes from PgBouncer, and the same mistake now shows up as a silent hang: which port Prisma Migrate needs on Supabase, and how to keep both URLs in Prisma 6 and 7.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
7 min read

TL;DR#

npx prisma migrate dev stopped with FATAL: bouncer config error when its URL pointed at Supabase's pooled port (6543), which ran PgBouncer in 2022. The message is PgBouncer's: the pooler closed a connection before its login completed. Today's shared pooler, Supavisor, does not send it, and the same mistake makes the command hang instead. Give the Prisma CLI port 5432, either the direct connection or the session pooler, and on serverless keep 6543 for your application with a separate URL: directUrl in schema.prisma on Prisma 5 and 6, datasource.url in prisma.config.ts on Prisma 7.

The error#

You add a model, run the first migration against a fresh Supabase project, and instead of a migration you get this:

text
Error: db error: FATAL: bouncer config error
   0: migration_core::state::DevDiagnostic
             at migration-engine/core/src/state.rs:251

A developer hit this exact error on Stack Overflow in December 2022, running npx prisma migrate dev --name init against a Supabase database. The same command migrated successfully against a Postgres database on Railway, the second run in the asker's screenshot. Nothing in the Prisma schema was wrong: only what the connection string pointed at had changed.

The accepted answer, with 31 upvotes, replaces the port number in the connection string, 6543, with 5432, because "6543 is the pooled port number which should not be used when migrating, instead the non-pooled connection string using 5432 should be used." That fix is right, and the error message explains more than it seems.

Why it happens#

The message comes from PgBouncer#

bouncer config error is not a Prisma message. It appears in PgBouncer's source, and in neither Prisma's engine nor Supavisor's code. PgBouncer sends it only while it is still logging a client in. In the 2022 releases (1.17.0 and 1.18.0) that covers three cases: auth_type=any without a forced user, PAM combined with database authentication, and a server connection that drops while the client is still logging in (objects.c). Later releases add authentication-database checks, and PgBouncer's own tests produce the message with an authentication query that fails. So in December 2022 a PgBouncer answered on Supabase's port 6543, and it refused a connection at login.

The span trace says where that happened. Its state.rs:251 is the DevDiagnostic span of Prisma's migration engine, on that line in every release from 4.4.0 through 4.7.1 (4.8.0 moved it), so the asker ran one of those. In that step, migrate dev "reruns the existing migration history in the shadow database", in the words of Prisma's development workflow docs. Without a shadowDatabaseUrl, that engine runs CREATE DATABASE on the main connection, then connects to the new database with the same URL, only the database name changed: same host, same port, same pooler. Supabase never documented which login check its pooler failed. What both vendors do document is the rule the accepted answer follows.

Migrations do not belong on the transaction pooler#

Supabase's connection guide sends "migrations, pg_dump, backup and restore, or replication" to the direct connection, because "these are single sessions and Postgres native commands". It also lists four things the transaction pooler does not support: prepared statements, WITH HOLD cursors, session-level state such as session-level advisory locks, and query pipelining. Prisma's PgBouncer page says its schema engine "is designed to use a single connection to the database, and does not support connection pooling with PgBouncer", and that CLI commands should use a direct connection string.

The ports have moved since 2022. Today the guide maps them like this:

  • 6543 reaches Supavisor in transaction mode on the shared pooler host, or PgBouncer when it is the dedicated pooler that paid plans get at db.[PROJECT-REF].supabase.co:6543. The connection goes back to the pool after each transaction, which suits serverless and edge functions.
  • 5432 reaches Postgres directly at db.[PROJECT-REF].supabase.co, or Supavisor in session mode on the shared pooler host. Session mode is the alternative to the direct connection from IPv4-only networks, because the direct connection is IPv6 unless the project has the IPv4 add-on.

The same mistake has produced a different symptom each time the pooler changed. In January 2024, Prisma issue #22779 reported prepared statement "s0" does not exist from migrations and from npx prisma db push through Supavisor's transaction mode. In a reproduction on 6 October 2026 with Supavisor 2.9.13, migrate dev on 6543 printed its three loading lines and waited until killed; the same project migrated on 5432. That silent variant is covered in Prisma Migrate Dev Hangs? Fix the Supabase Pooler Stall. The fix has not changed: the CLI goes to port 5432.

Fix#

1. Get the connection strings from Supabase#

Open the Connect panel of your project. It lists the direct connection, the session pooler (ending in 5432) and the transaction pooler (ending in 6543). Copy the strings rather than composing hosts by hand: Supabase warns that the pooler host cannot be derived from your region name. [POOLER-HOST] below stands for that host.

2. Point migrations at port 5432#

The fastest fix, and the one accepted on the original Stack Overflow thread, is to change the port:

.env
bash
# was: ...@[POOLER-HOST]:6543/postgres
DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:5432/postgres"
bash
npx prisma migrate dev --name init

That string is the session pooler. The direct connection, postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres, works as well where IPv6 is available. For a long-running server, one session-mode string is enough: Supabase's Prisma guide uses it for migrations and for the application.

3. Serverless: keep 6543 for the app, give the CLI its own URL#

If the application runs in serverless functions, keep the transaction pooler for Prisma Client and give the CLI a second URL. On Prisma 5 and 6 that is directUrl:

prisma/schema.prisma
prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}
.env
bash
DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:5432/postgres"

pgbouncer=true is for Prisma Client: Supabase's Prisma troubleshooting page says it "turns off prepared statements in Prisma", which transaction mode does not support. The CLI connects through directUrl without any flag: in a reproduction, db push with this block and DATABASE_URL still on 6543 reported at "supavisor:5432" in its Datasource line.

On Prisma 7 the URLs leave schema.prisma. Prisma 7.10.0 rejects both fields there with error P1012, "The datasource property directUrl is no longer supported in schema files". The upgrade guide puts the former directUrl value in datasource.url of prisma.config.ts, which the CLI uses:

prisma.config.ts
ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
 
export default defineConfig({
  schema: "prisma/schema.prisma",
  datasource: {
    url: env("DIRECT_URL"),
  },
});

The application then passes the 6543 string to its driver adapter, new PrismaPg({ connectionString: process.env.DATABASE_URL }), as in Prisma's PgBouncer page.

If you're paste-checking a connection string right now, run it through the tool below: it parses the host, port, and pooler flags and tells you which mode you're actually pointed at, without sending the string anywhere.

Runs entirely in your browser — nothing you paste is sent anywhere, and the password is masked in every result. Still cautious? Replace the password with any string containing the same special characters: the diagnosis only needs its shape.

Verify the fix#

bash
npx prisma migrate dev --name init

The Datasource line now names port 5432, and the run ends with the migration applied. In the reproduction, on Supavisor's session port:

text
Datasource "db": PostgreSQL database "postgres", schema "public" at "supavisor:5432"
 
Applying migration `20261006000744_init`
 
The following migration(s) have been created and applied from new schema changes:
 
prisma/migrations/
  └─ 20261006000744_init/
    └─ migration.sql
 
Your database is now in sync with your schema.

Prisma 6 then regenerates Prisma Client. Prisma 7 no longer does, so run npx prisma generate yourself there. If the run still stops with bouncer config error, the URL still reaches a PgBouncer, such as the dedicated pooler on db.[PROJECT-REF].supabase.co:6543: check the host and the port in the string you just edited.

Then check the history:

bash
npx prisma migrate status
text
1 migration found in prisma/migrations
 
Database schema is up to date!

Finally, confirm the app still answers through its own URL by running a query from your normal dev server. With directUrl or prisma.config.ts in place, only the CLI moved to 5432; Prisma Client keeps the pooled string.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the shadow database is also blocked#

Once the CLI gets through, migrate dev still has to create its shadow database, and a role without the CREATEDB privilege stops there with error P3014. The message begins "Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases." Switching ports does not fix it. On Supabase, the default postgres role has the privilege: Supabase's Postgres image grants it CREATEDB, and the custom prisma user in Supabase's Prisma guide is created with bypassrls createdb. P3014 there usually means a custom role created without it. Prisma's shadow database docs name Heroku, Digital Ocean and Vercel Postgres as hosts where you may have to create the shadow database yourself and point shadowDatabaseUrl at it. That workflow is covered in Prisma Migrate P3014: permission denied to create database.

If Prisma rejects the URL itself, the string is usually malformed: when a special character in the password breaks how the URL parses, Prisma reports invalid port number even though the port is right; Prisma Can't Connect to PostgreSQL: Fix invalid port shows the percent-encoding.

Further reading: Fix npx prisma db push Hanging on Supabase (Port 6543). Also see Fix Prisma "Could not figure out an ID in create" panic.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote