diff --git a/lib/supabase/browser.ts b/lib/supabase/browser.ts index 6250f89..2d45694 100644 --- a/lib/supabase/browser.ts +++ b/lib/supabase/browser.ts @@ -1,10 +1,15 @@ 'use client'; import { createBrowserClient } from '@supabase/ssr'; +import { supabaseAuthCookieName } from './cookie-name'; export function createSupabaseBrowserClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + // Explicit (equals the default it derives from the public URL) so the + // server clients can pin the identical name while talking to the internal + // API host. Keeps both sides on the same auth cookie. + { cookieOptions: { name: supabaseAuthCookieName() } }, ); } diff --git a/lib/supabase/cookie-name.ts b/lib/supabase/cookie-name.ts new file mode 100644 index 0000000..0f0c634 --- /dev/null +++ b/lib/supabase/cookie-name.ts @@ -0,0 +1,18 @@ +/** + * The auth cookie name that @supabase/ssr would derive from the PUBLIC Supabase + * URL (e.g. `sb-api-auth-token` for `https://api.linumiq.net`). + * + * Server-side clients talk to Supabase over the internal Docker URL + * (SUPABASE_INTERNAL_URL, e.g. `http://kong-prod:8000`) to bypass the edge rate + * limiter. But @supabase/ssr derives the cookie storage key from whatever URL + * it is given, so a server client pointed at the internal host would look for + * `sb-kong-prod-auth-token` while the browser set `sb-api-auth-token` — the + * session cookie would never be found and getUser() would return null. Pinning + * every client to this single, public-URL-derived name keeps the browser and + * server cookie namespaces in sync regardless of the API endpoint used. + */ +export function supabaseAuthCookieName(): string { + const publicUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const ref = new URL(publicUrl).hostname.split('.')[0]; + return `sb-${ref}-auth-token`; +} diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts index 09fe272..5508233 100644 --- a/lib/supabase/server.ts +++ b/lib/supabase/server.ts @@ -1,5 +1,6 @@ import { createServerClient } from '@supabase/ssr'; import { cookies } from 'next/headers'; +import { supabaseAuthCookieName } from './cookie-name'; export function createSupabaseServerClient() { const cookieStore = cookies(); @@ -9,6 +10,9 @@ export function createSupabaseServerClient() { process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { + // Pin the cookie name to the public-URL-derived value so it matches the + // browser client even though we point the API at the internal host. + cookieOptions: { name: supabaseAuthCookieName() }, cookies: { getAll() { return cookieStore.getAll(); diff --git a/middleware.ts b/middleware.ts index 0ae3fd2..43aae39 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,6 @@ import { createServerClient } from '@supabase/ssr'; import { NextResponse, type NextRequest } from 'next/server'; +import { supabaseAuthCookieName } from '@/lib/supabase/cookie-name'; // Path prefixes that are reachable without a completed MFA step-up. These are // the auth flow itself, the MFA enrollment/challenge surface, their supporting @@ -34,6 +35,9 @@ export async function middleware(request: NextRequest) { process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { + // Pin the cookie name to the public-URL-derived value so it matches the + // browser client even though the API points at the internal host. + cookieOptions: { name: supabaseAuthCookieName() }, cookies: { getAll() { return request.cookies.getAll();