Use the session pooler on 5432 for DDL; the direct host is IPv6-only

Transaction mode (6543) drops session state and prepared statements; the direct db host has no IPv4. Session mode on the pooler's 5432 gives you both.

Migrations & Pooling

· Chapter

6

·

2

min read

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 via PGPASSWORD or 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