# Supabase + Next.js 16: authorize with getClaims(), not getSession()

> Source: <https://dev.to/anthonypan99/supabase-nextjs-16-authorize-with-getclaims-not-getsession-4423>
> Published: 2026-10-20 02:09:56+00:00

**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 <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](https://loadout-blush.vercel.app/packs/nextjs-supabase)

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