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', () => {