Supabase Auth
Auth session missing! — and why getSession is not the fix
Luca Urti
AuthSessionMissingError: Auth session missing!
Why does Supabase say “Auth session missing” when the user is signed in?
The client instance handling this call has no session in the storage it was given. In the browser that usually means a second client was constructed that does not share storage with the one that signed in, or the call ran before the session was restored from storage on page load. On the server it almost always means the request's cookies never reached the client you created, because a server client is built per request and has no ambient session to fall back on. The important part is what you do next: the two methods that look interchangeable here are not, and only one of them is safe to trust on a server.
getUser verifies, getSession does not
getSession() reads whatever is in storage — a cookie, local storage — and hands it back. It does not check the token's signature against the Auth server. getUser() sends the token to be revalidated and returns a user only if it holds up. In the browser that difference costs a network round trip and rarely matters. On a server it is the whole security boundary: a cookie is client-supplied input, and anything that reads it without verification is trusting a value the caller controls.
Supabase's own documentation states this without hedging — never trust getSession() inside server code, because it is not guaranteed to revalidate the token. Treat any server-side authorisation decision made from getSession() as unauthenticated.
Constructing the client per request
A server client must be created inside the request handler and given that request's cookies, and it must be able to write refreshed cookies back onto the response. A module-level client shared across requests either has no session at all — this error — or, far worse, holds the session of whoever hit the server first and serves their data to the next visitor.
The fix
the server-side check that actually authenticates
export async function handler(req: Request, res: Response) {
// Per request, never at module scope: a shared client serves the first
// visitor's session to everybody who arrives after them.
const supabase = createServerClient(url, anonKey, {
cookies: cookieAdapter(req, res),
});
// getUser() sends the token to be revalidated. This is the authentication.
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) return unauthorized(res);
// user.id is now safe to authorize against.
return loadDataFor(user.id);
// NOT this on a server — it returns the cookie's contents without checking
// the signature, so the caller decides who they are:
// const { data: { session } } = await supabase.auth.getSession();
// if (session) allow(session.user.id);
}The fix that works and costs you the database
Because getSession() returns something where getUser() returned an error, it reads like the working alternative — and on a server it is an authentication bypass. It hands back the contents of a cookie without verifying the signature, so a request carrying a hand-written token gets an object with whatever sub it chose. Every ownership check downstream then compares against an attacker-supplied id. The second version of the same shortcut is giving up on the session and querying with the service_role key, which removes the question of who the user is by removing Row Level Security along with it.
Whether the trap is already in your repo is a question you can answer
Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.