Fix Supabase "Invalid Refresh Token" and "Auth session missing!"
You were signed in a minute ago. You refreshed the page, opened a second tab, or clicked around as usual — and now you are on the login screen. The console says Invalid Refresh Token, Refresh Token Not Found, or the clie
You were signed in a minute ago. You refreshed the page, opened a second tab, or clicked around as usual — and now you are on the login screen. The console says Invalid Refresh Token, Refresh Token Not Found, or the client throws Auth session missing!.
This is one of the most searched Supabase auth errors around Lovable, Bolt, Vite and other apps that use supabase-js. Usually the session was revoked, or the client is holding a refresh token the server no longer accepts.
Below are the checks I would run, in order. Everything here is taken from the User sessions guide, the Auth error codes table, the server-side Auth advanced guide, and the PKCE flow page, all read on 8 November 2026.
What a session actually is
A session starts when a user signs in. Supabase represents it with two values: a short-lived access token (a JWT; most apps should keep the default expiry of 1 hour) and a refresh token. The docs are explicit: refresh tokens "never expire but can only be used once." You exchange one for a new pair, and the old refresh token is spent.
The error-codes table maps the messages you see:
-
refresh_token_not_found: "Session containing the refresh token not found." -
refresh_token_already_used: the token "has been revoked and falls outside the refresh token reuse interval." -
session_not_found: the session no longer exists — the user signed out, or the row was deleted. -
session_expired: an inactivity timeout or a configured maximum lifetime was reached. -
conflict: "too many session refresh requests firing off at the same time for a user."
The JavaScript client also throws AuthSessionMissingError, with the message Auth session missing!. The error-codes guide describes the matching Kotlin exception the same way: the session is missing "if the user was logged out or deleted." getSession() can return null when the user "is not signed-in or has logged out."
1. Refresh-token reuse detection
Supabase treats a reused refresh token as a possible theft — for example a token that leaked in logs or cookies. If a reuse attempt does not fall under two exceptions, "the whole session is regarded as terminated and all refresh tokens belonging to it are marked as revoked."
The two exceptions:
- The same refresh token may be used more than once within a reuse interval. The default is 10 seconds, and they do not recommend changing it. This covers SSR, where the server and then the browser both refresh shortly after, and races when two requests refresh at once.
- If the parent of the currently active refresh token is used, the active token is returned. That covers a client that never received the previous response and later retries the token it already used.
Anything else ends the session: refresh_token_already_used, or refresh_token_not_found if the session row is already gone. You can disable reuse detection in Advanced Settings, but the docs say that is generally not recommended.
2. Multiple tabs refreshing at once
Two tabs, or two createClient() instances on the same page, often share one stored refresh token. Both try to refresh. The first succeeds and gets a new pair. The second still sends the old token. If that request falls outside the exceptions above, the entire session is revoked.
The conflict code "can often occur when you have too many session refresh requests firing off at the same time for a user." The client library has a long-standing report of this multi-tab race (supabase/gotrue-js#213). You can still trip it when a tab keeps a stale token, a React effect fires many refreshes, or you call setSession or refreshSession with an old token.
refreshSession() will throw if the current refresh token is invalid. Keep a single browser client, let it auto-refresh, and listen for TOKEN_REFRESHED rather than issuing parallel refreshes.
3. signOut() logging everyone out
The default scope of signOut() is global. That "signs the user out of every device they are currently signed in on, not just the current tab/session." The server-side guide calls this a common cause of invalid refresh token errors: a signOut() without a scope revokes every session that user has, including other tabs, devices, and any SSR context still holding the old token.
If you only want to end the current session:
const { error } = await supabase.auth.signOut({ scope: 'local' })
global invalidates every session first, then clears the local one. others invalidates every other session and keeps the current one. In the apps we check, a generated "Log out" button often calls signOut() with no options.
4. Server-side code seeing a stale token
If the error appears in a server log rather than the browser console, the advanced guide's first guess is that "the refresh token sent from the browser to your server is stale." Make sure the onAuthStateChange listener is registered early and free of bugs. When the server hits this error, they recommend deferring that render to the browser, where the client can use an up-to-date token.
A related trap: getSession() "loads values directly from the storage attached to the client." If that storage is request cookies, the docs advise against trusting those values for authorization — use getUser() instead. getClaims() only checks the JWT signature and expiry; "an unexpired token stays valid even when the session behind it was revoked." Call getUser() where that gap matters.
5. Magic link or confirm email opened in another browser
If the user requested a magic link or confirmation email in one browser and opened it in another browser or device, PKCE will fail. The docs are unambiguous: "The code verifier is created and stored locally when the Auth flow is first initiated. That means the code exchange must be initiated on the same browser and device where the flow was started."
The auth code in the URL is also single-use and valid for five minutes. Retrying the same link, or starting a second sign-in before the first completes (which overwrites the stored verifier), produces flow_state_not_found, flow_state_expired or bad_code_verifier. For the flow-state errors, the docs say to ask the user to sign in again.
If the link itself opens localhost instead of your production domain, that is a Site URL problem, not a refresh-token problem. I wrote up that fix separately: Supabase Email Link Goes to localhost.
6. Storage not persisting the session
By default, supabase-js sets persistSession to true and stores the session in local storage. If the environment has no storage — a server client without a custom adapter, or a misconfigured SSR setup — you get the warning: "No storage option exists to persist the session, which may result in unexpected behavior when using auth."
The official server-side example turns persistence off on purpose:
const supabase = createClient(supabase_url, publishable_key, {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false
}
})
Do not use those flags in the browser client that should keep the user signed in. If the server and browser need to share a session, use the storage option, or @supabase/ssr, which stores the session in cookies and uses PKCE by default. If nothing is written to storage, every reload looks like a new visit and getSession() comes back empty.
7. The session really is gone
Sometimes the error is not a bug. A session also ends when the user signs out, changes their password or takes another security-sensitive action, times out from inactivity or hits a maximum lifetime (Pro plans and up), or when "Single session per user" is enabled and they signed in on another device. Then only the most recent session stays active.
Those settings are not applied immediately to every existing session. The check runs the next time the session is refreshed, so "the actual duration of a session is the configured timeout plus the JWT expiration time."
When getSession() returns no session, or refreshSession() errors because the refresh token is invalid, do not keep retrying the same token. Show the sign-in screen.
How to see what the client thinks is happening
Register a listener early, and log the event name:
const { data } = supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
})
And read the current session from storage when you need it:
const { data, error } = await supabase.auth.getSession()
Identify errors with error.code and error.name, not by matching the message string. If event is SIGNED_OUT and nobody clicked log out, walk the list above.
Most of the sudden logouts I see come back to a refresh token used twice outside the reuse window, a signOut() that defaulted to every device, or a session that was never written to storage. Once the token is gone, the user has to sign in again.
If a confirmation or magic-link email is landing on the wrong host, start with that localhost write-up. And if you are about to launch, our free Launch Check looks at headers, exposed keys and more, with no sign-up.
I wrote this with AI assistance and reviewed it myself.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.