Jeśli kiedyś trafiłeś na błąd prepared statement "s0" already exists albo na timeout w Vercelu przy prisma.user.findMany(), problem prawie zawsze jest ten sam: używasz złego URL-a.
TL;DR
# .env.local
DATABASE_URL="postgresql://...pooler.supabase.com:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://...db.supabase.co:5432/postgres"// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // do queries (transakcje krótkie)
directUrl = env("DIRECT_URL") // do migracji (transakcje długie)
}Dlaczego dwa URL-e
DATABASE_URL przechodzi przez pgBouncer w trybie transaction. Supabase ustawia to domyślnie. Dzięki temu możesz mieć 100 instancji serverless funkcji i każda dostaje połączenie z puli, zamiast otwierać 100 równoległych klientów do Postgresa.
Minus: nie wspiera prepared statements przy reużyciu. Prisma tego standardowo używa. Dlatego w URL-u dopisujesz ?pgbouncer=true — Prisma wtedy rozpoznaje, że nie może na nich polegać.
DIRECT_URL to bezpośrednie połączenie do bazy. Używane przez prisma migrate, bo migracje to długie transakcje DDL (CREATE TABLE, ALTER COLUMN), których pooler by nie przepuścił.
Typowe błędy
1. Brak ?pgbouncer=true
Objaw: losowe prepared statement "s0" already exists w produkcji, na lokalu wszystko działa.
2. Ten sam URL w obu polach
Objaw: prisma migrate dev wisi w nieskończoność albo timeout po 2 min.
3. DIRECT_URL w produkcji bez potrzeby
Jeśli nie odpalasz migracji z produkcji — nie potrzebujesz tam DIRECT_URL w ogóle. Ustaw go tylko w CI i lokalnie.
Sanity check
// Quick runtime check
await prisma.$queryRaw`SELECT current_setting('application_name')`;
// pooler connections pokazują np. "PostgREST/12"
// direct — nic albo nazwę Twojej apkiTyle. Wraca się do tej pułapki co projekt, więc teraz mam notatkę.