Skip to content

Security basics

Supabase "Auth session missing!": where the session went

"Auth session missing!" is supabase-js finding no signed-in session and stopping before it asks Supabase. Where the session went, and the fix for each.

Vlad Tkachenko11 min read
An empty badge holder on a lanyard, the space where the badge belongs outlined in amber, in front of two badges that carry a face.

In short

  • "Auth session missing!" (AuthSessionMissingError, status 400) comes from supabase-js inside your app. It looked for a signed-in session, found none, and never sent the request to Supabase.
  • On a page nobody has signed in to, that is the correct answer and your code only has to handle it. On a password reset page it means the link never signed anyone in here. In an Edge Function or on a server, the visitor's session was never passed along.
  • Leave getUser() where it is. Swapping in getSession() on a server makes the error go away by trusting whatever the cookie says.

Your Lovable or Bolt app signs people in with Supabase. One morning you open the browser console and find this in red, on a page where nothing looks wrong:

AuthSessionMissingError: Auth session missing!

Or a customer writes in to say the "set a new password" page refuses them with the same words, every time.

Search "auth session missing supabase" and you will find threads full of fixes: switch getUser() to getSession(), downgrade the library, add a delay, restart the project. They assume something between your app and Supabase broke. No request was made. The error is written inside your app, by the Supabase library, before anything leaves the browser.

What does "Auth session missing!" mean in Supabase?

supabase-js was asked who is signed in, looked for a session, found none, and answered on its own.

A session is what Supabase Auth hands a person when they sign in: a pair of tokens that say who they are, kept by the browser so the next page load still knows them. supabase-js is the library your app uses to talk to Supabase, and every call that needs a person starts by taking out that session.

The message is written in supabase-js itself, in the auth library's errors.ts: an AuthSessionMissingError with the text Auth session missing! and the status 400. The 400 is a label the library gives itself. No server answered with it, and Supabase's logs will show nothing, because getUser() checks for a session before it does anything else. Its source says so in a comment: it "returns an error if there is no access_token or custom authorization header".

Picture a front desk that can tell you which visitor someone is. Everyone who signs in is given a badge at the door, and supabase-js keeps that badge in the browser's pocket. When your code asks "who is this?", supabase-js reaches into the pocket first. If there is a badge, it walks it to the desk and brings back a name. If the pocket is empty, it does not walk to the desk at all. It comes straight back and tells you there is no badge.

With a session, supabase-js asks Supabase Auth. Without one, it stops in your app and the error comes from there.

It is handed back, not thrown

getUser() returns the error next to an empty user, and your code decides what happens next.

The call looks like this, and with no session the result is user set to null and error set to the AuthSessionMissingError:

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

Nothing is thrown, so a try … catch around the call catches nothing. That is where the visible damage comes from, in one of two shapes:

  • The code shows any error it gets. A visitor who has not signed in yet sees "Auth session missing!" in a toast or a red box on a page they were allowed to see.
  • The code uses the user without checking. It reads user.id and the page breaks with Cannot read properties of null, which looks like a different bug entirely.

Both are the same mistake: treating "nobody is signed in" as a failure. Check the user first, and give a signed-out visitor the screen meant for them:

const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  // nobody is signed in: show the sign-in screen, or the public page
  return
}

If you want to tell this apart from a real failure, the library names it: error.name is 'AuthSessionMissingError'.

Where the session goes missing

Three places, and only the first one is harmless.

1. Nobody has signed in on this browser. Your app calls getUser() on a page anyone can open, perhaps to decide whether to show "Sign in" or "My account". For a visitor, the empty pocket is the true answer. This is where most people meet the message, and the code fix above is all it needs.

2. A password reset or magic link page. Setting a new password calls updateUser({ password }), and Supabase's password guide is plain about the order: "Once you have a session, collect the user's new password and call updateUser." On that page, the session is supposed to come from the link in the email, which signs the person in as it opens. When it does not, the pocket is empty and updateUser() answers "Auth session missing!". The usual reasons:

  • The link had already been used, or had expired, before it was opened. Why Supabase says the email link is invalid or has expired goes through each one.
  • It was opened in a different browser or app from the one that asked for it. When an app uses Supabase's PKCE sign-in, part of the exchange is kept in the browser that started it, and Supabase's documentation says "the code exchange must be initiated on the same browser and device where the flow was started".
  • It sent the person somewhere else, such as localhost, so the session landed on an address that is not your app. Why Supabase redirects to localhost has that one.

3. Your server or an Edge Function never received the session. The badge lives in your visitor's browser. Code that runs anywhere else, a Supabase Edge Function or the server half of a Next.js app built with v0 or Cursor, creates its own Supabase client, and a new client starts with an empty pocket. Unless the visitor's session is passed in, getUser() there finds nothing and answers "Auth session missing!" without asking Supabase.

In an Edge Function the session travels in the request's Authorization header, and supabase-js only uses it when you hand it over while creating the client. In a Next.js app it travels in cookies, which Supabase's server-side helper reads only if you give it the request's cookies. If an Edge Function hands that failure back as an error, your app shows "Edge Function returned a non-2xx status code" with this message waiting in the function's log.

The session belongs to the browser. A function or server only has it if the request carries it and the code passes it to its client.

How to tell which one you have

Look at where the message appears, then at what the browser actually sent.

In the browser console, on a page anyone can open. Route 1. Open your browser's developer tools (F12), choose Application, then Local Storage and your app's address. supabase-js keeps the session under a key shaped like sb- followed by your project's id and -auth-token. If there is no such entry, this browser has no session, and the message is telling the truth. A Next.js app using Supabase's server-side helpers keeps the session in cookies instead, so look under Cookies for the same name.

On a reset or magic link page. Route 2. Look at the address the link opened on. If it is not your app, or it is a link you or a mail scanner already opened, the session never had a chance to arrive.

In an Edge Function's log, or your server's. Route 3. Open the Network tab, repeat the action and click the request to your function. If its Authorization header holds a long string beginning eyJ, the browser sent a person's token and your function did not pass it on to its client.

One thing you will not find, on any route, is a request to /auth/v1/user that came back with this message. If you do see one that failed, it is a different error. A reply saying invalid claim: missing sub claim means your project key was sent where a person belongs, and that post is its twin.

The fix for each route

Give each one the session it is missing, or an answer that does not need one.

Route 1, the signed-out visitor: check user before using it, as in the code above, and show the screen meant for someone who has not signed in.

Route 2, the reset page: wait for the link to sign the person in before you show the form. Supabase sends a PASSWORD_RECOVERY event when someone arrives from a recovery link, and its reference shows the page listening for it:

supabase.auth.onAuthStateChange((event) => {
  if (event === 'PASSWORD_RECOVERY') {
    // the link signed them in: now show the new-password form
  }
})

If the event never comes, the link did not work. Say so, and offer to send a new one, rather than showing a form that is certain to fail.

Route 3, the function or server: pass the visitor's session to the client you create. In an Edge Function, create it with the incoming header:

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!,
  { global: { headers: { Authorization: req.headers.get('Authorization')! } } },
)
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  return new Response(JSON.stringify({ error: 'Sign in first' }), {
    status: 401,
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
}

Supabase's newer withSupabase({ auth: 'user' }) wrapper does the same before your handler runs, and the Edge Function post shows the setup. Keep the 401 for a caller with no session, so a request from a signed-out page gets a clear refusal. With the header passed on, a signed-out caller's getUser() there may come back with invalid claim: missing sub claim instead: the same visitor seen from the other side, and the 401 covers both.

In a Next.js app, create the server client with Supabase's @supabase/ssr helper and the request's cookies, as its server-side setup shows, and call getUser() on that client.

Two fixes that make it worse

Both get suggested for this message, and each one stops the error by removing the check behind it.

Swapping getUser() for getSession(). In the browser it changes nothing that matters: the storage is still empty, so you get no session and no user, only without the message. On a server it is dangerous. Supabase's server-side guide says "Never trust supabase.auth.getSession() inside server code", because "It reads the session out of the cookie without revalidating it." A cookie is something the visitor sends you, and anyone can send you any cookie they like. getUser() is the one supabase-js documents as making "a network request to the Supabase Auth server, so the returned value is authentic". Keep it wherever the answer decides what someone may do.

Putting the secret key into the client so the call stops failing. A client made with the service_role or sb_secret_ key does not need a session to read and write, because it skips your database rules entirely. The function works again, for every caller, signed in or not. If a builder makes that change in code that ships to the browser, the key goes out to every visitor with it. If it already has, rotate it first.

You can check that from outside. Our free scan reads your live app's JavaScript the way a stranger would and names every key it finds, including whether it is one that belongs there. It takes about 20 seconds and needs no account: scan your app.

Keeping the check in place after the next change

The check on who is signed in is the part of a page most likely to vanish when you ask your builder to make an error go away, and both of the quick fixes above remove it.

Reeve Monitor re-scans your live app every hour:

  • all nine checks every hour, on up to three apps
  • an email the day a re-scan grades your app lower than the one before; a secret key in your JavaScript caps the grade at D on its own
  • whether the app is up, every 60 seconds
  • a monthly report of what it saw

A server that believes any cookie, or a function running on the secret key, lets a stranger change your users' rows. Reeve Care keeps a copy of your Supabase database from before that happens.

  • an encrypted copy on a schedule, kept on Reeve's own storage in the EU, outside your Supabase account
  • each copy verified before it counts, by counting the rows in every table
  • a one-click restore that saves what is there now before it replaces anything
  • your uploaded files as well, once you connect a Storage credential
  • everything Monitor does

What to do now

What to do

  • Read "Auth session missing!" as "nobody is signed in here". The request never left your app.
  • Check user for null before using it, and give a signed-out visitor the screen meant for them instead of the error.
  • On a reset page, wait for the PASSWORD_RECOVERY event before showing the form, and offer a new link when it never arrives.
  • In an Edge Function or on a server, pass the visitor's Authorization header or cookies to the client you create, and answer 401 when there is still no user.
  • Keep getUser() on the server. getSession() there believes whatever cookie it is sent.

If your sessions exist and then disappear, so that people who were signed in are suddenly signed out, that is a different problem with its own causes, and why Supabase users get logged out covers it. The 10-minute security checklist covers what else a newly launched app tends to leave open.

FAQ

What does "Auth session missing!" mean in Supabase?

It means supabase-js, the Supabase library inside your app, was asked for the signed-in user and had no session to ask with. A session is what Supabase Auth gives a person when they sign in, and the library keeps it in the browser. With none there, it does not contact Supabase at all. It hands back AuthSessionMissingError with the status 400 and a user of null.

Why do I see "Auth session missing!" when nobody is logged in?

Because that is the answer for a visitor who has not signed in. Your app called getUser() on a page that anyone can open, and supabase-js reported that nobody was signed in. Nothing is broken. Your code only has to read user as null and show the sign-in screen, instead of showing the error.

Why does my password reset page say "Auth session missing!"?

Changing a password with updateUser() needs a session, and on a reset page that session is supposed to come from the link in the email. If the link had already been used or had expired, was opened in a different browser from the one that asked for it, or sent the person to a different address, no session arrived and updateUser() has nobody to change. Wait for the PASSWORD_RECOVERY event before showing the form, and offer a fresh link when it never comes.

Is "Auth session missing!" the same as "invalid claim: missing sub claim"?

No. "Auth session missing!" is decided inside your app, before any request leaves it. "invalid claim: missing sub claim" comes back from Supabase Auth, which received a request and found your project key where a person's token should be. The first means there was nothing to send. The second means the wrong thing was sent.

Should I use getSession() instead of getUser()?

Not to make this error go away. In the browser, getSession() finds the same empty storage and returns no session, so nothing changes except that the error message stops. On a server it is worse: supabase-js documents that getSession() reads the session straight from storage such as request cookies, which may not be authentic, and recommends getUser() instead because it checks with Supabase Auth.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Back up your Supabase database before your next change

Reeve keeps an encrypted copy on a schedule and restores it for you if something breaks.

See Supabase backups

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.