fix(connection-manager): skip direct URL derivation for Session Pooler (port 6543) — fixes IPv6-only ECONNREFUSED#1006
Open
diazMelgarejo wants to merge 1 commit into
Conversation
…port 6543) ## Problem deriveDirectUrl() fired for any Supabase pooler URL — both Transaction Pooler (port 5432) and Session Pooler (port 6543) — always producing db.<ref>.supabase.co:5432. That hostname is IPv6-only on most Supabase projects, causing ECONNREFUSED for the majority of users on standard IPv4 networks. ## Root cause The dual-pool architecture (read pool → pooler, DDL pool → direct) was designed to work around Transaction Pooler's constraints: - Stateless (no session across transactions) - 2-minute statement_timeout enforced by PgBouncer Neither constraint applies to Session Pooler (port 6543), which runs PgBouncer in session mode: full session state, configurable timeouts, prepared statements, and native support for DDL. ## Fix Return null from deriveDirectUrl() when port === '6543'. With no direct URL, isDualPoolActive() returns false and ddl()/bulk() fall back to the read pool (Session Pooler) — which handles DDL correctly. Before: port 6543 URL → derives db.<ref>.supabase.co:5432 → IPv6 fail After: port 6543 URL → null → ddl() uses Session Pooler directly ✓ Transaction Pooler (port 5432 on *.pooler.supabase.com) is unchanged: it still derives the direct URL because it cannot handle DDL. ## Escape hatch Users who need a dedicated DDL pool (e.g. for 30-min schema migrations on very large databases) can set GBRAIN_DIRECT_DATABASE_URL explicitly to an IPv4-accessible direct or tunneled connection. ## Tests updated connection-manager.serial.test.ts: rewrote the deriveDirectUrl suite to cover Session Pooler (null) vs Transaction Pooler (derives direct) as distinct cases. Added ConnectionManager routing test for both. Closes: garrytan/gstack#1301 See also: garrytan/gstack#1249 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
2 files, ~15 lines.
deriveDirectUrl()now returnsnullfor Session Pooler URLs (port 6543), preventing the auto-derivation ofdb.<ref>.supabase.co:5432— which is IPv6-only on most Supabase projects and causesECONNREFUSEDfor users on standard IPv4 networks.Closes garrytan/gstack#1301
See also: gstack IPv6 link-local fix garrytan/gstack#1249
Companion band-aid PR (9 migration files): #1005
The problem
connection-manager.tsauto-derives a "direct" DDL connection URL from any Supabase pooler URL:On standard IPv4 networks — which is the majority of home/office/CI environments — connecting to
db.<ref>.supabase.co:5432fails withECONNREFUSEDbecause Supabase resolves that hostname to an IPv6 address (AAAArecord only).This crashes:
gbrain init --migrate-only(DDL → tries direct pool)gbrain apply-migrations --yes(orchestrators spawn subprocesses that also hit this)ddl()orbulk()Why the current design was correct for Transaction Pooler but wrong for Session Pooler
The dual-pool architecture exists for a good reason: Transaction Pooler (port 5432 on
*.pooler.supabase.com) cannot handle DDL because:statement_timeoutwhich kills long schema migrationsstatement_timeout,maintenance_work_mem) don't persist across transactionsSo for Transaction Pooler, deriving a direct URL and opening a separate DDL pool is the right design.
Session Pooler (port 6543) has none of these constraints:
statement_timeoutSession Pooler IS a full session — it's PgBouncer in session mode. It can handle
ALTER TABLE,CREATE INDEX CONCURRENTLY, long migrations, everything. There is no architectural reason to derive a separate direct URL for it.The fix
Result:
deriveDirectUrl()returns null →isDualPoolActive()false →ddl()uses read pool (Session Pooler) → works on IPv4GBRAIN_DIRECT_DATABASE_URLexplicit override → still respected → escape hatch intactWhat about the 30-minute DDL statement_timeout?
The direct pool was configured with
statement_timeout: 30min. The read pool via Session Pooler gets the timeout fromresolveSessionTimeouts()— which defaults to 5min unless overridden.For most users and most migrations, 5 minutes is sufficient. For very large databases with long-running schema changes, the escape hatch is:
This could be a Supabase IPv6-capable host (VPN/Tailscale), a connection pooler you run yourself, or the direct URL once your network has IPv6 support. This env var already exists in the code — we're just documenting it as the power-user path.
Tests
Updated
test/connection-manager.serial.test.ts:deriveDirectUrlsuite to explicitly distinguish Session Pooler (→ null) vs Transaction Pooler (→ derives direct)ConnectionManagerrouting tests for both pooler typesContrast with band-aid PR #1005
_childEnv()× 9GBRAIN_DISABLE_DIRECT_POOL=1in child envIf this PR merges, #1005 becomes unnecessary. If only #1005 merges, future migration files will need the same
_childEnv()pattern added manually.Happy to PR this fix if useful. Verified on gbrain v0.33.2.1, macOS 26.x, Supabase Session Pooler (port 6543).