{"slug": "supabase-next-js-16-authorize-with-getclaims-not-getsession", "title": "Supabase + Next.js 16: authorize with getClaims(), not getSession()", "summary": "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.", "body_md": "**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.\n\nIf you let an AI agent write the auth code in a Supabase + Next.js app, check one thing first: **what your server code trusts.**\n\nAgents (and plenty of older tutorials) still produce this in Server Components, Server Actions, and route handlers:\n\n```\nconst { data: { session } } = await supabase.auth.getSession()\nif (!session) redirect('/login')\n\nconst userId = session.user.id // used to decide what this request can read or write\n```\n\nIt looks fine and it works in testing. The problem is where the data comes from.\n\nOn 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.\n\nIf `session.user.id` decides which rows a request can read or write, a forged cookie decides it too.\n\n`getClaims()` verifies the access token's signature before it returns the claims:\n\n`/.well-known/jwks.json`). The keys are usually cached, so this is fast.` getUser()`.\nA protected Server Component:\n\n``` js\n// app/dashboard/page.tsx\nimport { redirect } from 'next/navigation'\nimport { createClient } from '@/lib/supabase/server'\n\nexport default async function DashboardPage() {\n  const supabase = await createClient()\n  const { data, error } = await supabase.auth.getClaims()\n\n  if (error || !data?.claims) redirect('/login')\n\n  const userId = data.claims.sub // verified subject (the user's id)\n\n  const { data: projects } = await supabase\n    .from('projects')\n    .select('id, name')\n    .eq('owner_id', userId) // keep RLS on as well; this filter is not your only lock\n\n  return <ProjectList projects={projects ?? []} />\n}\n```\n\n| You need | Use | \n|---|---|\n| To protect a page, Server Action, route handler, or query | `getClaims()` | \n| An up-to-date user record from the Auth server | `getUser()` (makes a network call) | \n| The raw access or refresh token, e.g. to forward to another service | `getSession()` ,**never for authorization** | \n\nA Server Action can be called over HTTP even if no button in your UI calls it. Treat each one like an API route: validate input, verify claims, then write.\n\nIf you want the agent to stop inventing the old pattern, the free Next.js + Supabase Loadout pack includes this as a pinned rule: [https://loadout-blush.vercel.app/packs/nextjs-supabase](https://loadout-blush.vercel.app/packs/nextjs-supabase)\n\nCanonical fix page: [https://loadout-blush.vercel.app/fixes/supabase-ssr-getsession-vs-getclaims](https://loadout-blush.vercel.app/fixes/supabase-ssr-getsession-vs-getclaims)", "url": "https://wpnews.pro/news/supabase-next-js-16-authorize-with-getclaims-not-getsession", "canonical_source": "https://dev.to/anthonypan99/supabase-nextjs-16-authorize-with-getclaims-not-getsession-4423", "published_at": "2026-10-20 02:09:56+00:00", "updated_at": "2026-10-20 02:16:53.329008+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["Supabase", "Next.js", "Vercel"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/supabase-next-js-16-authorize-with-getclaims-not-getsession", "markdown": "https://wpnews.pro/news/supabase-next-js-16-authorize-with-getclaims-not-getsession.md", "text": "https://wpnews.pro/news/supabase-next-js-16-authorize-with-getclaims-not-getsession.txt", "jsonld": "https://wpnews.pro/news/supabase-next-js-16-authorize-with-getclaims-not-getsession.jsonld"}}