Skip to content

Commit b340200

Browse files
fix(auth): pass user ID from middleware via header to eliminate redundant getUser() calls
The middleware already validates the token via getUser() — server components were calling getUser() again (a network call each time), causing Supabase Auth rate limits. Now passes x-user-id header from middleware, and getUserRole() + isSuperAdmin() read from it instead of making redundant auth API calls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a4561f9 commit b340200

3 files changed

Lines changed: 31 additions & 16 deletions

File tree

lib/supabase/get-user-role.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
import { createClient } from '@/lib/supabase/server'
22
import { createAdminClient } from '@/lib/supabase/admin'
3-
import { getCurrentTenantId } from '@/lib/supabase/tenant'
3+
import { getCurrentTenantId, getCurrentUserId } from '@/lib/supabase/tenant'
44

55
/**
66
* Get the user's role for the current tenant.
77
* First checks tenant_users table (authoritative for multi-tenant),
88
* then falls back to JWT claims.
9+
*
10+
* Uses x-user-id header (set by middleware) to avoid redundant getUser() calls.
911
*/
1012
export async function getUserRole(): Promise<'student' | 'teacher' | 'admin' | null> {
11-
const supabase = await createClient()
12-
13-
const {
14-
data: { user },
15-
} = await supabase.auth.getUser()
16-
17-
if (!user) {
18-
return null
19-
}
13+
const userId = await getCurrentUserId()
14+
if (!userId) return null
2015

2116
// Check tenant_users for the current tenant (authoritative source)
17+
const supabase = await createClient()
2218
const tenantId = await getCurrentTenantId()
2319
const { data: membership } = await supabase
2420
.from('tenant_users')
2521
.select('role')
26-
.eq('user_id', user.id)
22+
.eq('user_id', userId)
2723
.eq('tenant_id', tenantId)
2824
.eq('status', 'active')
2925
.single()
@@ -79,17 +75,18 @@ export async function getUserTenantId(): Promise<string | null> {
7975
/**
8076
* Check if current user is a super admin by querying the super_admins table directly.
8177
* Does NOT trust JWT claims to prevent privilege escalation.
78+
*
79+
* Uses x-user-id header (set by middleware) to avoid redundant getUser() calls.
8280
*/
8381
export async function isSuperAdmin(): Promise<boolean> {
84-
const supabase = await createClient()
85-
const { data: { user } } = await supabase.auth.getUser()
86-
if (!user) return false
82+
const userId = await getCurrentUserId()
83+
if (!userId) return false
8784

8885
const adminClient = createAdminClient()
8986
const { data } = await adminClient
9087
.from('super_admins')
9188
.select('user_id')
92-
.eq('user_id', user.id)
89+
.eq('user_id', userId)
9390
.maybeSingle()
9491

9592
return !!data

lib/supabase/tenant.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ export async function getCurrentTenantId(): Promise<string> {
6161
return headersList.get('x-tenant-id') || DEFAULT_TENANT_ID
6262
}
6363

64+
/**
65+
* Get the current user ID from headers (set by middleware after getUser() validation).
66+
* Returns null for unauthenticated requests.
67+
* Use this instead of supabase.auth.getUser() in server components to avoid
68+
* redundant network calls to Supabase Auth (prevents rate limiting).
69+
*/
70+
export async function getCurrentUserId(): Promise<string | null> {
71+
const headersList = await headers()
72+
return headersList.get('x-user-id') || null
73+
}
74+
6475
/**
6576
* Get the default tenant (for single-tenant backward compat).
6677
*/

proxy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,16 @@ export default async function proxy(request: NextRequest) {
219219
// Re-using the user here avoids a second getUser() network call.
220220
const { response: supabaseResponse, user } = await updateSession(request)
221221

222-
// Set tenant ID header on the response for downstream server components
222+
// Set tenant + user ID headers on the response for downstream server components.
223+
// This lets server components read the user ID from headers instead of calling
224+
// getUser() again (which is a network call to Supabase Auth on every invocation).
223225
intlResponse.headers.set('x-tenant-id', tenantId)
224226
supabaseResponse.headers.set('x-tenant-id', tenantId)
227+
if (user) {
228+
request.headers.set('x-user-id', user.id)
229+
intlResponse.headers.set('x-user-id', user.id)
230+
supabaseResponse.headers.set('x-user-id', user.id)
231+
}
225232

226233
// Single Supabase client used for the rest of this middleware run.
227234
// getSession() reads from cookie (no network call) — safe because updateSession

0 commit comments

Comments
 (0)