Server-Side getUser() Migrated to Local JWT Validation
What Changed
lib/supabase/server.ts:getUser() now uses supabase.auth.getClaims() instead of supabase.auth.getUser(). Every Server Action that identifies the caller (billing, admin, user settings, profile updates) now validates the JWT signature locally against Supabase's cached asymmetric public key set, instead of making a network round trip to the Supabase auth API.
The proxy-side rollout in v1.8.3 covered the route guard. This release extends the same pattern to the per-action identity check, which is where most of the cumulative latency lives. A typical login + dashboard load + a couple of settings clicks involves 5 to 10 Server Action calls. At 50 to 200ms per round trip depending on cross-region pairing, that's roughly 250ms to 2 seconds saved per user session.
What Ships in the Template
getUser() returns a normalized identity type
The return type changes from Supabase's full User object to a normalized AuthUser containing only what the template's callsites actually read:
export type AuthUser = {
id: string
email: string
}
export async function getUser(): Promise<AuthUser | null> {
const supabase = await createServerClientWithCookies()
const { data } = await supabase.auth.getClaims()
const claims = data?.claims
if (!claims || !claims.email) return null
return {
id: claims.sub,
email: claims.email,
}
}
The 5 callsites in the template (actions/admin.ts, actions/billing.ts (2 places), actions/user.ts, app/(dashboard)/billing/page.tsx) only ever read user.id and user.email, so no callsite changes were needed.
One downstream prop type changed: components/layout/dashboard-header.tsx previously typed its user prop as User from @supabase/supabase-js; it now types it as AuthUser from lib/supabase/server.
Breaking Change Note
If your project has customized Server Actions that read fields beyond user.id and user.email (for example user.user_metadata, user.app_metadata, user.created_at, user.phone), they will fail to compile after this change. Two paths to fix:
- Extend the
AuthUsertype inlib/supabase/server.tsand read the additional fields fromclaimsingetUser(). Most JWT-resident fields (role,phone,app_metadata,user_metadata) are available on theclaimsobject per theJwtPayloadtype in@supabase/auth-js. - For fields not in the JWT claims (like
created_at), pull them from theprofilestable viagetUserWithProfile()or a fresh admin client query.
Prerequisites
Asymmetric JWT signing keys must be enabled on the Supabase project. New projects ship with them by default since 2025; existing projects enable them in the Supabase dashboard under Authentication → Keys.
What Did NOT Change
getUserWithProfile()unchanged; it just calls the newgetUser()and queries the profile row byuser.id.- The cookie-based session client (
createServerClientWithCookies) unchanged. - The admin client (
createAdminClient) unchanged. next.config.tsunchanged.- Marketing/blog/login routes still use the
next.config.tsbaseline CSP; the strict nonce-based CSP introduced in v1.8.3 still covers only/dashboard,/settings,/billing,/admin.
Upgrading Existing Projects
Replace the getUser() body in lib/supabase/server.ts with the getClaims() version above, add the AuthUser type export, and update any component that types its user prop as the Supabase User to use AuthUser instead. Run npm run typecheck to catch any callsite that reads fields beyond id and email; extend AuthUser for those.