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

21

·

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 Record each migration's checksum in the same transaction as its DDL. 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: Record each migration's checksum in the same transaction as its DDL · Self-hosted Supabase: only Kong should face the internet