From 49fd6b65b9a0e501850bff15bd6662ff8c9f4f2f Mon Sep 17 00:00:00 2001 From: Zaid Marzguioui Date: Wed, 1 Apr 2026 13:53:59 +0200 Subject: [PATCH] fix(supabase): use internal URL for all server-side clients with cookie name matching ROOT CAUSE FIX: All server-side Supabase clients (server-client, middleware-client, server-admin-client) now use SUPABASE_INTERNAL_URL (http://supabase-kong:8000) when available, with cookieOptions.name set to match the external URL's cookie key (e.g. sb-myeasycms-auth-token). This gives us: - Reliable Docker-internal networking (no hairpin NAT through Traefik) - Correct session cookie matching between browser and server - No more 500 errors on SSR pages that query Supabase Reverted per-page try/catch workarounds since root cause is now fixed. --- Dockerfile | 2 +- .../[locale]/home/[account]/meetings/page.tsx | 19 +++-------- .../[locale]/home/[account]/verband/page.tsx | 9 +----- docker-compose.yml | 4 ++- .../supabase/src/clients/middleware-client.ts | 24 ++++++++++++-- .../src/clients/server-admin-client.ts | 3 +- .../supabase/src/clients/server-client.ts | 32 ++++++++++++++++++- 7 files changed, 64 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 421a4a52b..d20cf643f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app # --- Install + Build in one stage --- FROM base AS builder # CACHE_BUST: change this value to force a full rebuild (busts Docker layer cache) -ARG CACHE_BUST=13 +ARG CACHE_BUST=14 RUN echo "Cache bust: ${CACHE_BUST}" COPY . . RUN pnpm install --no-frozen-lockfile diff --git a/apps/web/app/[locale]/home/[account]/meetings/page.tsx b/apps/web/app/[locale]/home/[account]/meetings/page.tsx index 2241668fb..76ef7c22b 100644 --- a/apps/web/app/[locale]/home/[account]/meetings/page.tsx +++ b/apps/web/app/[locale]/home/[account]/meetings/page.tsx @@ -23,20 +23,11 @@ export default async function MeetingsPage({ params }: PageProps) { const api = createMeetingsApi(client); - let stats = { totalProtocols: 0, thisYearProtocols: 0, openTasks: 0, overdueTasks: 0 }; - let recentProtocols: Awaited> = []; - let overdueTasks: Awaited> = []; - - try { - [stats, recentProtocols, overdueTasks] = await Promise.all([ - api.getDashboardStats(acct.id), - api.getRecentProtocols(acct.id), - api.getOverdueTasks(acct.id), - ]); - } catch (e) { - // Supabase query failed — render with empty data instead of crashing - console.error('Failed to load meetings dashboard:', e); - } + const [stats, recentProtocols, overdueTasks] = await Promise.all([ + api.getDashboardStats(acct.id), + api.getRecentProtocols(acct.id), + api.getOverdueTasks(acct.id), + ]); return ( diff --git a/apps/web/app/[locale]/home/[account]/verband/page.tsx b/apps/web/app/[locale]/home/[account]/verband/page.tsx index 3dffdd964..558674223 100644 --- a/apps/web/app/[locale]/home/[account]/verband/page.tsx +++ b/apps/web/app/[locale]/home/[account]/verband/page.tsx @@ -22,14 +22,7 @@ export default async function VerbandPage({ params }: PageProps) { if (!acct) return ; const api = createVerbandApi(client); - - let stats = { totalClubs: 0, totalMembers: 0, totalRoles: 0, totalFeeTypes: 0 }; - - try { - stats = await api.getDashboardStats(acct.id); - } catch (e) { - console.error('Failed to load verband dashboard:', e); - } + const stats = await api.getDashboardStats(acct.id); return ( diff --git a/docker-compose.yml b/docker-compose.yml index 310a601ff..aecdd0c3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -321,10 +321,12 @@ services: environment: NODE_ENV: production NEXT_PUBLIC_SITE_URL: ${SITE_URL:-http://localhost:3000} - # Same URL for browser AND server — keeps Supabase cookie names consistent + # Same URL for browser AND server cookie names NEXT_PUBLIC_SUPABASE_URL: ${API_EXTERNAL_URL:-http://localhost:8000} NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY} NEXT_PUBLIC_DEFAULT_LOCALE: de + # Internal URL for server-side Supabase calls (Docker network, no hairpin NAT) + SUPABASE_INTERNAL_URL: http://supabase-kong:8000 SUPABASE_SECRET_KEY: ${SUPABASE_SERVICE_ROLE_KEY} SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret} EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de} diff --git a/packages/supabase/src/clients/middleware-client.ts b/packages/supabase/src/clients/middleware-client.ts index 23d3e3bdd..d9f784486 100644 --- a/packages/supabase/src/clients/middleware-client.ts +++ b/packages/supabase/src/clients/middleware-client.ts @@ -9,8 +9,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; /** * Creates a middleware client for Supabase. * - * @param {NextRequest} request - The Next.js request object. - * @param {NextResponse} response - The Next.js response object. + * Uses SUPABASE_INTERNAL_URL when available for reliable Docker networking, + * with cookieOptions.name matching the external URL's cookie key. */ export function createMiddlewareClient( request: NextRequest, @@ -18,7 +18,15 @@ export function createMiddlewareClient( ) { const keys = getSupabaseClientKeys(); - return createServerClient(keys.url, keys.publicKey, { + const internalUrl = process.env.SUPABASE_INTERNAL_URL; + const url = internalUrl || keys.url; + + const cookieOptions = internalUrl + ? { name: deriveCookieName(keys.url) } + : undefined; + + return createServerClient(url, keys.publicKey, { + ...(cookieOptions ? { cookieOptions } : {}), cookies: { getAll() { return request.cookies.getAll(); @@ -35,3 +43,13 @@ export function createMiddlewareClient( }, }); } + +function deriveCookieName(supabaseUrl: string): string { + try { + const hostname = new URL(supabaseUrl).hostname; + const ref = hostname.split('.')[0]!; + return `sb-${ref}-auth-token`; + } catch { + return 'sb-localhost-auth-token'; + } +} diff --git a/packages/supabase/src/clients/server-admin-client.ts b/packages/supabase/src/clients/server-admin-client.ts index 1cb82cd04..3d002ff00 100644 --- a/packages/supabase/src/clients/server-admin-client.ts +++ b/packages/supabase/src/clients/server-admin-client.ts @@ -15,7 +15,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; export function getSupabaseServerAdminClient() { warnServiceRoleKeyUsage(); - const url = getSupabaseClientKeys().url; + const keys = getSupabaseClientKeys(); + const url = process.env.SUPABASE_INTERNAL_URL || keys.url; const secretKey = getSupabaseSecretKey(); return createClient(url, secretKey, { diff --git a/packages/supabase/src/clients/server-client.ts b/packages/supabase/src/clients/server-client.ts index 8ce1b6a74..c2f87e182 100644 --- a/packages/supabase/src/clients/server-client.ts +++ b/packages/supabase/src/clients/server-client.ts @@ -9,11 +9,27 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; /** * @name getSupabaseServerClient * @description Creates a Supabase client for use in the Server. + * + * In Docker deployments, the server can optionally use SUPABASE_INTERNAL_URL + * (e.g. http://supabase-kong:8000) for faster, more reliable connections. + * The cookieOptions.name is set to match the external URL's cookie name + * so session cookies from the browser are correctly read. */ export function getSupabaseServerClient() { const keys = getSupabaseClientKeys(); - return createServerClient(keys.url, keys.publicKey, { + // Use internal URL for server-side requests if available + const internalUrl = process.env.SUPABASE_INTERNAL_URL; + const url = internalUrl || keys.url; + + // When using an internal URL, we must set the cookie name to match + // the external URL's cookie key so the server can read browser cookies. + const cookieOptions = internalUrl + ? { name: deriveCookieName(keys.url) } + : undefined; + + return createServerClient(url, keys.publicKey, { + ...(cookieOptions ? { cookieOptions } : {}), cookies: { async getAll() { const cookieStore = await cookies(); @@ -36,3 +52,17 @@ export function getSupabaseServerClient() { }, }); } + +/** + * Derives the Supabase cookie name from a URL, matching @supabase/ssr behavior. + * e.g. "https://myeasycms.frontieralgorithmics.de" → "sb-myeasycms-auth-token" + */ +function deriveCookieName(supabaseUrl: string): string { + try { + const hostname = new URL(supabaseUrl).hostname; + const ref = hostname.split('.')[0]!; + return `sb-${ref}-auth-token`; + } catch { + return 'sb-localhost-auth-token'; + } +}