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.
This commit is contained in:
@@ -5,7 +5,7 @@ WORKDIR /app
|
|||||||
# --- Install + Build in one stage ---
|
# --- Install + Build in one stage ---
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
# CACHE_BUST: change this value to force a full rebuild (busts Docker layer cache)
|
# 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}"
|
RUN echo "Cache bust: ${CACHE_BUST}"
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm install --no-frozen-lockfile
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
|||||||
@@ -23,20 +23,11 @@ export default async function MeetingsPage({ params }: PageProps) {
|
|||||||
|
|
||||||
const api = createMeetingsApi(client);
|
const api = createMeetingsApi(client);
|
||||||
|
|
||||||
let stats = { totalProtocols: 0, thisYearProtocols: 0, openTasks: 0, overdueTasks: 0 };
|
const [stats, recentProtocols, overdueTasks] = await Promise.all([
|
||||||
let recentProtocols: Awaited<ReturnType<typeof api.getRecentProtocols>> = [];
|
api.getDashboardStats(acct.id),
|
||||||
let overdueTasks: Awaited<ReturnType<typeof api.getOverdueTasks>> = [];
|
api.getRecentProtocols(acct.id),
|
||||||
|
api.getOverdueTasks(acct.id),
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CmsPageShell account={account} title="Sitzungsprotokolle">
|
<CmsPageShell account={account} title="Sitzungsprotokolle">
|
||||||
|
|||||||
@@ -22,14 +22,7 @@ export default async function VerbandPage({ params }: PageProps) {
|
|||||||
if (!acct) return <AccountNotFound />;
|
if (!acct) return <AccountNotFound />;
|
||||||
|
|
||||||
const api = createVerbandApi(client);
|
const api = createVerbandApi(client);
|
||||||
|
const stats = await api.getDashboardStats(acct.id);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CmsPageShell account={account} title="Verbandsverwaltung">
|
<CmsPageShell account={account} title="Verbandsverwaltung">
|
||||||
|
|||||||
@@ -321,10 +321,12 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
NEXT_PUBLIC_SITE_URL: ${SITE_URL:-http://localhost:3000}
|
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_URL: ${API_EXTERNAL_URL:-http://localhost:8000}
|
||||||
NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY}
|
NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY}
|
||||||
NEXT_PUBLIC_DEFAULT_LOCALE: de
|
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_SECRET_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
|
||||||
SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret}
|
SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret}
|
||||||
EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de}
|
EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
|
|||||||
/**
|
/**
|
||||||
* Creates a middleware client for Supabase.
|
* Creates a middleware client for Supabase.
|
||||||
*
|
*
|
||||||
* @param {NextRequest} request - The Next.js request object.
|
* Uses SUPABASE_INTERNAL_URL when available for reliable Docker networking,
|
||||||
* @param {NextResponse} response - The Next.js response object.
|
* with cookieOptions.name matching the external URL's cookie key.
|
||||||
*/
|
*/
|
||||||
export function createMiddlewareClient<GenericSchema = Database>(
|
export function createMiddlewareClient<GenericSchema = Database>(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
@@ -18,7 +18,15 @@ export function createMiddlewareClient<GenericSchema = Database>(
|
|||||||
) {
|
) {
|
||||||
const keys = getSupabaseClientKeys();
|
const keys = getSupabaseClientKeys();
|
||||||
|
|
||||||
return createServerClient<GenericSchema>(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<GenericSchema>(url, keys.publicKey, {
|
||||||
|
...(cookieOptions ? { cookieOptions } : {}),
|
||||||
cookies: {
|
cookies: {
|
||||||
getAll() {
|
getAll() {
|
||||||
return request.cookies.getAll();
|
return request.cookies.getAll();
|
||||||
@@ -35,3 +43,13 @@ export function createMiddlewareClient<GenericSchema = Database>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
|
|||||||
export function getSupabaseServerAdminClient<GenericSchema = Database>() {
|
export function getSupabaseServerAdminClient<GenericSchema = Database>() {
|
||||||
warnServiceRoleKeyUsage();
|
warnServiceRoleKeyUsage();
|
||||||
|
|
||||||
const url = getSupabaseClientKeys().url;
|
const keys = getSupabaseClientKeys();
|
||||||
|
const url = process.env.SUPABASE_INTERNAL_URL || keys.url;
|
||||||
const secretKey = getSupabaseSecretKey();
|
const secretKey = getSupabaseSecretKey();
|
||||||
|
|
||||||
return createClient<GenericSchema>(url, secretKey, {
|
return createClient<GenericSchema>(url, secretKey, {
|
||||||
|
|||||||
@@ -9,11 +9,27 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
|
|||||||
/**
|
/**
|
||||||
* @name getSupabaseServerClient
|
* @name getSupabaseServerClient
|
||||||
* @description Creates a Supabase client for use in the Server.
|
* @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<GenericSchema = Database>() {
|
export function getSupabaseServerClient<GenericSchema = Database>() {
|
||||||
const keys = getSupabaseClientKeys();
|
const keys = getSupabaseClientKeys();
|
||||||
|
|
||||||
return createServerClient<GenericSchema>(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<GenericSchema>(url, keys.publicKey, {
|
||||||
|
...(cookieOptions ? { cookieOptions } : {}),
|
||||||
cookies: {
|
cookies: {
|
||||||
async getAll() {
|
async getAll() {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
@@ -36,3 +52,17 @@ export function getSupabaseServerClient<GenericSchema = Database>() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user