Skip to content
All guides

Supabase getSession vs getUser: which one to use in server code

By Obscura Security · Updated · Supabase, Next.js, Cursor, Lovable, Bolt

Short answer

On the server, supabase.auth.getSession() only reads the session out of the request's cookies. It does not check with Supabase that the session is real. Use supabase.auth.getUser() for any server-side login check, or getClaims() if your project uses asymmetric JWT signing keys and you understand the trade-off.

If your Next.js (or other server-side) code decides who is logged in by calling supabase.auth.getSession(), the check can be fooled.

Why this happens

Supabase’s auth library stores the session in cookies. On the server, getSession() reads those cookies and hands the session back, including a user object. It does not ask Supabase whether the session is still valid, and it does not verify the token’s signature.

Cookies are sent by the browser, so whoever sends the request controls them. A request with a hand-edited cookie can carry a session that says the user is someone else. If your code then reads session.user.id and uses it to look up data, you’ve trusted something the caller wrote.

getUser() works differently. It sends the access token to the Supabase Auth server on every call, and the server confirms the token is genuine and the user still exists. Supabase’s own documentation says never to trust getSession() inside server code such as middleware, and to use getUser() there.

AI tools get this wrong often. getSession() is the shorter call, it appears in older tutorials, and it works fine in a demo. The code passes every test you run as a normal logged-in user.

Where it does the damage depends on what your server does next:

  • A route handler that uses session.user.id with the service role key or an ORM. Nothing else checks the ID, so a forged one reads or changes someone else’s rows.
  • A role or admin check built on the session’s user object. See admin pages that are only hidden in the interface.
  • A deleted or banned user. Their old cookie keeps passing a getSession() check until the token runs out.

How to check your own app

Do this against your own code.

  1. Search your server-side files for every use of getSession:
grep -rn "auth.getSession" app src lib utils middleware.ts

Adjust the folders to match your project. Ignore matches in files that run only in the browser (components marked 'use client').

  1. For each match in middleware, a route handler, a server action, or a server component, look at what happens after. If the result decides who the user is or what they can see, it needs fixing.
  2. Optionally, test the deleted user case. Sign in to your own app with a test account, delete that user in the Supabase dashboard, then reload a page that uses getSession() on the server. If the page still loads as that user, the check isn’t asking Supabase.

The fix

1. Use getUser() wherever the server decides who someone is

// app/api/orders/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'

export async function GET() {
  const supabase = await createClient()

  const {
    data: { user },
    error,
  } = await supabase.auth.getUser()

  if (error || !user) {
    return NextResponse.json({ error: 'Unauthorised' }, { status: 401 })
  }

  const { data: orders } = await supabase
    .from('orders')
    .select('*')
    .eq('user_id', user.id)

  return NextResponse.json(orders)
}

The code that was there before probably looked like this:

// Not safe on the server
const { data: { session } } = await supabase.auth.getSession()
if (!session) return NextResponse.json({ error: 'Unauthorised' }, { status: 401 })
const userId = session.user.id

createClient here is the helper from Supabase’s Next.js server-side auth guide, built on @supabase/ssr. Use the one your project already has.

2. Keep getUser() in middleware

Next.js middleware (renamed proxy in Next.js 16) is where Supabase’s template refreshes the session cookie on each request. That template calls getUser(), and it should stay that way. Redirecting on a getSession() result there is the same mistake with a bigger reach, because it guards every page.

3. Check again inside each protected route

Middleware is a first gate. Call getUser() again in each route handler and server action that touches private data, as in the example above. Don’t rely on the page having been redirected earlier.

4. Use getClaims() only if you are sure

On newer projects with asymmetric JWT signing keys, getClaims() verifies the token against your project’s public keys, usually without a network call:

const { data, error } = await supabase.auth.getClaims()
if (error || !data?.claims) {
  return NextResponse.json({ error: 'Unauthorised' }, { status: 401 })
}
const userId = data.claims.sub

It’s quicker, and it’s sound for reading who the token belongs to. It can’t tell that a user was deleted or signed out after the token was issued, because the token stays valid until it expires. If your project still uses the older shared-secret signing, getClaims() falls back to asking the Auth server, so it behaves like getUser(). When in doubt, use getUser().

Mistakes to avoid

  • Using getSession() “just to read the ID” and getUser() elsewhere. The ID you read is the one that can be forged.
  • Fixing the middleware but not the route handlers. Handlers can be reached directly, so each needs its own check.
  • Reading the user from a cookie you set yourself. A plain userId cookie is the same problem with extra steps.
  • Assuming RLS covers it. RLS only protects queries made with the user’s own token. Queries that use the service role key skip it, which is why the ID has to be right. See Supabase RLS disabled for what RLS does cover.

Common questions

Is getSession() safe to use in the browser?
Yes. In client-side code the session comes from the user's own browser storage and you are only using it to show or hide things. The problem is using it as proof of identity on the server, where the cookie can be edited by whoever sends the request.
Why does Supabase log a warning about getSession() on the server?
Recent versions of the library warn when you use the user object from getSession() in server code, because that object came from cookie storage and was not verified. Swap the call for getUser() and the warning goes away.
Is getUser() slower than getSession()?
Yes, a little. getUser() makes a request to the Supabase Auth server each time, and getSession() does not. For checking who a request belongs to, that extra request is the point.
When is getClaims() okay?
When your project uses asymmetric JWT signing keys, getClaims() checks the token's signature against your project's public keys without a network call to the Auth server. The catch is that a token stays valid until it expires, even if the user has since been deleted or signed out elsewhere. If you need that to take effect straight away, use getUser().
Does RLS protect me if my server code trusts getSession()?
Only for queries made with the user's own token, because Supabase checks that token itself. It does not help when your server uses the user ID from the session for something else, such as a query with the service role key, an ORM, or a decision about who is an admin.

Written by Obscura Security, an Australian practice that checks web apps for problems like this one. If you want a second pair of eyes on yours, email [email protected].