Skip to content

Commit 957698f

Browse files
fix(rls): forward x-tenant-id header to PostgREST for anon tenant context
Anon users on non-default tenant subdomains had no tenant_id in JWT, so get_tenant_id() defaulted to wrong tenant and RLS blocked all queries. Fix: server Supabase client now forwards x-tenant-id header (set by proxy.ts) to PostgREST. Updated get_tenant_id() reads request.headers JSON as fallback when JWT has no tenant_id claim. Priority chain: JWT claims > x-tenant-id header > default tenant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 730dab5 commit 957698f

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

lib/supabase/server.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createServerClient } from '@supabase/ssr'
2-
import { cookies } from 'next/headers'
2+
import { cookies, headers } from 'next/headers'
33

44
// Derive root domain for cross-subdomain cookie sharing
55
function getCookieDomain(): string | undefined {
@@ -15,13 +15,23 @@ function getCookieDomain(): string | undefined {
1515
* function when using it.
1616
*/
1717
export async function createClient() {
18-
const cookieStore = await cookies()
18+
const [cookieStore, headersList] = await Promise.all([cookies(), headers()])
1919
const cookieDomain = getCookieDomain()
20+
const tenantId = headersList.get('x-tenant-id')
2021

2122
return createServerClient(
2223
process.env.NEXT_PUBLIC_SUPABASE_URL!,
2324
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,
2425
{
26+
// Forward tenant ID to PostgREST so get_tenant_id() can read it
27+
// via current_setting('request.header.x-tenant-id'). This is
28+
// critical for anon users on non-default tenants where the JWT
29+
// has no tenant_id claim.
30+
global: {
31+
headers: {
32+
...(tenantId ? { 'x-tenant-id': tenantId } : {}),
33+
},
34+
},
2535
cookies: {
2636
getAll() {
2737
return cookieStore.getAll()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- =============================================================================
2+
-- Fix get_tenant_id() to read x-tenant-id header as fallback for anon users
3+
--
4+
-- Problem: On non-default tenant subdomains, anon (unauthenticated) users have
5+
-- no tenant_id in their JWT claims. get_tenant_id() falls back to the default
6+
-- tenant UUID, which means RLS blocks all queries for the actual tenant.
7+
--
8+
-- Fix: Read x-tenant-id from the request headers JSON (set by proxy.ts and
9+
-- forwarded by the Supabase server client). PostgREST exposes all request
10+
-- headers via current_setting('request.headers', true) as a JSON object.
11+
--
12+
-- Priority: JWT claims > request header > default tenant
13+
-- =============================================================================
14+
15+
CREATE OR REPLACE FUNCTION public.get_tenant_id()
16+
RETURNS uuid
17+
LANGUAGE sql
18+
STABLE
19+
SET search_path = ''
20+
AS $$
21+
SELECT COALESCE(
22+
-- 1. JWT claims (authenticated users with tenant context)
23+
(current_setting('request.jwt.claims', true)::jsonb ->>'tenant_id')::uuid,
24+
-- 2. Request header from proxy.ts (anon users on tenant subdomains)
25+
(current_setting('request.headers', true)::json ->>'x-tenant-id')::uuid,
26+
-- 3. Default tenant fallback
27+
'00000000-0000-0000-0000-000000000001'::uuid
28+
);
29+
$$;

0 commit comments

Comments
 (0)