cd /news/ai-agents/supabase-next-js-16-authorize-with-g… · home › topics › ai-agents › article
[ARTICLE · art-153409] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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.

by read2 min views5 publishedOct 20, 2026

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:

// 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 <ProjectList projects={projects ?? []} />
}
You need Use
To protect a page, Server Action, route handler, or query getClaims()
An up-to-date user record from the Auth server getUser() (makes a network call)
The raw access or refresh token, e.g. to forward to another service getSession() ,never for authorization

A 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.

If 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

Canonical fix page: https://loadout-blush.vercel.app/fixes/supabase-ssr-getsession-vs-getclaims

── more in #ai-agents 4 stories · sorted by recency
── more on @supabase 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/supabase-next-js-16-…] indexed:0 read:2min 2026-10-20 · —