From ddf2c6a9a0c04a009d718eaba6b4c20b81a43f8d Mon Sep 17 00:00:00 2001 From: Brandon Lipman Date: Fri, 8 May 2026 20:19:32 -0400 Subject: [PATCH] fix(connection-manager): strip . suffix from username when deriving direct URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `deriveDirectUrl()` correctly rewrites the host (`aws-0-us-east-1.pooler.supabase.com` → `db.abcxyz.supabase.co`) but preserves the full pooler-form username (`postgres.abcxyz`). Supabase direct connections expect a bare `postgres` username — Supavisor uses the `.` suffix for tenant routing, but it's not a real database user. The auto-derived URL therefore fails to authenticate even with the correct password: password authentication failed for user "postgres.abcxyz" Strip the suffix to `postgres` whenever the project-ref was successfully extracted (same condition that triggers the host rewrite). The non-pooler username branch is unaffected — preserved as-is to keep the port-only fallback case working. Hit while exercising v0.30.1's dual-pool routing on a real Supabase brain; the kill switch (`GBRAIN_DISABLE_DIRECT_POOL=1`) papered over it locally but every Supabase user with a stock pooler URL would silently fall through to single-pool until the user-supplied a `GBRAIN_DIRECT_DATABASE_URL` override. With this fix, dual-pool works out of the box for the canonical Supabase shape. Test additions: - 1 case asserting bare `postgres:secret@` in the derived URL when project-ref is parseable from the pooler URL (the new behavior) - extends the existing "falls back to port-only" case with an assertion that non-pooler usernames are preserved (unchanged behavior) `bun run typecheck` clean. `deriveDirectUrl` test block passes 5/5. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/connection-manager.ts | 10 ++++++++-- test/connection-manager.serial.test.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/core/connection-manager.ts b/src/core/connection-manager.ts index 4a4a447a4..73cd913b8 100644 --- a/src/core/connection-manager.ts +++ b/src/core/connection-manager.ts @@ -142,16 +142,22 @@ export function deriveDirectUrl(url: string): string | null { const decodedUser = decodeURIComponent(user); const refMatch = decodedUser.match(/^postgres\.([a-z0-9]+)$/i); let directHost = hostname; + let directUser = parsed.username; if (refMatch && refMatch[1] && isPoolerHost) { directHost = `db.${refMatch[1]}.supabase.co`; + // Supabase direct connections use bare `postgres`; the `postgres.` + // form is pooler-only (Supavisor uses the suffix for tenant routing). + // Without this strip, direct auth fails with `password authentication + // failed for user "postgres."` even though the password is correct. + directUser = 'postgres'; } // Compose direct URL by swapping host + port. Preserve auth, db, query. parsed.hostname = directHost; parsed.port = '5432'; // Reconstruct with the original scheme. const scheme = url.match(/^postgres(?:ql)?:\/\//i)?.[0] ?? 'postgres://'; - const auth = parsed.username - ? `${parsed.username}${parsed.password ? `:${parsed.password}` : ''}@` + const auth = directUser + ? `${directUser}${parsed.password ? `:${parsed.password}` : ''}@` : ''; const search = parsed.search ?? ''; const path = parsed.pathname ?? ''; diff --git a/test/connection-manager.serial.test.ts b/test/connection-manager.serial.test.ts index 619961e2c..42c1fd737 100644 --- a/test/connection-manager.serial.test.ts +++ b/test/connection-manager.serial.test.ts @@ -44,6 +44,18 @@ describe('deriveDirectUrl', () => { expect(direct).toContain(':secret@'); // creds preserved }); + test('strips . suffix from username when going pooler→direct', () => { + // Supabase direct connections require bare `postgres`; the `postgres.` + // form is pooler-only (Supavisor uses the suffix for tenant routing). + // Without the strip, direct auth fails with "password authentication + // failed for user postgres." even with the correct password. + const direct = deriveDirectUrl( + 'postgresql://postgres.abcxyz:secret@aws-0-us-east-1.pooler.supabase.com:6543/postgres' + ); + expect(direct).toContain('postgres:secret@'); // bare username + expect(direct).not.toContain('postgres.abcxyz:secret@'); // no pooler suffix + }); + test('falls back to port-only swap when project-ref unparseable', () => { const direct = deriveDirectUrl( 'postgresql://customuser:secret@some.pooler.supabase.com:6543/db' @@ -51,6 +63,7 @@ describe('deriveDirectUrl', () => { expect(direct).toBeTruthy(); expect(direct).toContain(':5432'); expect(direct).toContain('some.pooler.supabase.com'); // host preserved + expect(direct).toContain('customuser:secret@'); // non-pooler username preserved }); test('returns null for non-pooler URL', () => {