Supabase + Next.js 16: authorize with getClaims(), not getSession() A developer documented that Supabase server-side auth code generated by AI agents and older tutorials commonly uses getSession() for authorization, which loads the session from the user-controlled auth cookie without revalidating its signature. The recommended fix is getClaims(), which verifies the access token against the project's JWKS endpoint before returning claims, with the verified subject (claims.sub) used for row filtering alongside row-level security. The writeup also advises treating Server Actions as HTTP-callable API routes and reserving getSession() for forwarding raw tokens, never for authorization. AI disclosure: I wrote this article with AI assistance. I checked every claim and code sample against the Supabase and Next.js docs linked at the end. If you spot a mistake, tell me in the comments and I'll fix it. If you let an AI agent write the auth code in a Supabase + Next.js app, check one thing first: what your server code trusts. Agents and plenty of older tutorials still produce this in Server Components, Server Actions, and route handlers: const { data: { session } } = await supabase.auth.getSession if session redirect '/login' const userId = session.user.id // used to decide what this request can read or write It looks fine and it works in testing. The problem is where the data comes from. On the server, getSession loads the session from the auth cookie without revalidating it . A cookie is user input. The Supabase SSR guide says so directly: anyone can forge the session cookie, so never trust getSession inside server code. If session.user.id decides which rows a request can read or write, a forged cookie decides it too. getClaims verifies the access token's signature before it returns the claims: /.well-known/jwks.json . The keys are usually cached, so this is fast. getUser . A protected Server Component: js // app/dashboard/page.tsx import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' export default async function DashboardPage { const supabase = await createClient const { data, error } = await supabase.auth.getClaims if error || data?.claims redirect '/login' const userId = data.claims.sub // verified subject the user's id const { data: projects } = await supabase .from 'projects' .select 'id, name' .eq 'owner id', userId // keep RLS on as well; this filter is not your only lock return