The answer. Hosted Supabase gives you three ways in. The direct host (db.<ref>.supabase.co) is IPv6-only unless you buy the IPv4 add-on, so from many CI runners and laptops it simply doesn't connect. The pooler has IPv4, on two ports. Port 6543 is transaction mode: a backend is borrowed per transaction, which is what a serverless API wants, but anything that leans on session state breaks — named prepared statements, session-level set role / set, advisory locks held across transactions, migration runners that expect the same backend from first statement to last. Port 5432 on the same pooler host is session mode: a real, sticky Postgres session, over IPv4. Use 5432 for migrations and admin work, 6543 for the app.
The pattern.
# Session mode (migrations, admin): pooler host, port 5432, user postgres.<project-ref>
export PGPASSWORD="$SUPABASE_DB_PASSWORD" # never in the URL, never in argv
export DB_URL="postgresql://postgres.<project-ref>@<region>.pooler.supabase.com:5432/postgres"
psql "$DB_URL" -X -c "show server_version" # sanity: connects from an IPv4-only network
# Transaction mode (app runtime): same host, port 6543 — no session state, no prepared statements
# DATABASE_URL="postgresql://postgres.<project-ref>@<region>.pooler.supabase.com:6543/postgres"
# Why the direct host times out from IPv4-only networks:
dig +short AAAA db.<project-ref>.supabase.co # answers
dig +short A db.<project-ref>.supabase.co # empty
The receipt. In our product (HarperFlow), the production migration path settled on 2026-08-07, when migration 0244 was applied through the session pooler on 5432 with the tracked runner from tracked-migration-ledger-checksum-same-transaction. The memory note from that day records the two dead ends: the direct db.<ref> host is IPv6-only, and the transaction-mode port is marked unusable for the multi-statement runner. The same lesson turned up independently in a second codebase: our SEO platform's README tells deployers that Drizzle's migrator expects prepared-statement support the transaction-mode pooler doesn't provide, so its migration DATABASE_URL must be the session pooler or the direct connection. A release-staging project in the same region uses the same pooler shape. Latency or throughput differences between the two modes were not measured.
Watch out.
- The Management API token is not a substitute for knowing your connection: ours was dead (401) from 2026-08-07 until the owner rotated it on 2026-08-23, and it doesn't tell you which port your tool needs anyway.
- A password containing
@or other URL characters breaks URL-form connection strings. Pass it viaPGPASSWORDor use the keyword/value conninfo form. - Session mode holds a real backend per client. Don't point a serverless API at 5432 or you'll exhaust connections.
Related: tracked-migration-ledger-checksum-same-transaction · self-hosted-only-kong-faces-the-internet