Symptoms
- Logs show
AuthApiError: Invalid Refresh Token: Refresh Token Not Found. - Users get logged out at random, often after about an hour (when the access token expires and the app tries to refresh).
- Signing out on a phone logs the same user out on their laptop too.
- It happens in a Next.js app with server-side auth (middleware or route handlers), less often in a pure single-page app.
- On localhost, after resetting or switching the database, old cookies keep producing the error.
Why it happens
A Supabase session is a short-lived access token (a JWT) plus a refresh token. Supabase's docs say refresh tokens "never expire but can only be used once" (Supabase: User sessions). There's a short reuse window, 10 seconds by default, for legitimate cases such as server-side rendering. Outside it, reusing an old token counts as a possible theft: "the whole session is regarded as terminated and all refresh tokens belonging to it are marked as revoked."
The common causes:
- Global sign-out. In JavaScript,
signOut()defaults to theglobalscope. Supabase's docs say this "revokes the refresh tokens for every session that user has, including other devices and browsers. Those other sessions fail withAuthApiError: Invalid Refresh Token: Refresh Token Not Foundthe next time they try to refresh" (Supabase: Signing out). - Two refreshes racing. A browser tab and your SSR middleware both try to refresh the same session at the same moment. Refresh tokens are single use, so one side loses. Supabase's maintainers describe this in supabase/ssr#68.
- An old
@supabase/ssr. A background initialization bug made the server client refresh on its own and race yourgetUser()call. Per a Supabase maintainer in supabase/ssr#68, it "was fixed in@supabase/ssr@0.8.1". - A stale token on the server. Supabase's SSR guide says the most likely reason is that "the refresh token sent from the browser to your server is stale" (Supabase: Advanced SSR guide).
- Session limits you set. Time-boxed sessions, an inactivity timeout or "single session per user" in Auth settings end sessions on purpose. They're enforced at the next refresh (Supabase: User sessions).
Step-by-step fix
1. Fix sign-out
Find every supabase.auth.signOut() call. Unless you really want "log out of all devices", pass the local scope:
await supabase.auth.signOut({ scope: 'local' })2. Upgrade the Supabase packages
npm install @supabase/ssr@latest @supabase/supabase-js@latestIf you're on a Next.js app, check that @supabase/ssr is at least 0.8.1 afterwards.
3. Refresh in one place per request
Follow Supabase's current Next.js middleware setup (Supabase: Creating a client for SSR). A Supabase maintainer's advice in supabase/ssr#68: don't call getUser(), getClaims() or getSession() from both middleware and an API route on the same request.
4. Create server clients per request
Supabase warns that on Vercel's Fluid compute a client "initialized in module scope, or stored in a shared variable, may be reused across requests from different users" (Advanced SSR guide). Build the server client inside each request handler. Create the browser client once and reuse it.
5. Register onAuthStateChange early
Supabase's SSR guide says to make sure the onAuthStateChange listener "is free of bugs and is registered relatively early in your application's lifetime." When the server hits this error, let the browser handle the session (it has the freshest token) instead of crashing the page.
6. Leave reuse detection on
You can turn off refresh-token reuse detection in Auth settings, but Supabase says this is "generally not recommended" (User sessions). Fix the race instead. (Self-hosted only: a maintainer suggests setting GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL to a few seconds, because self-hosted projects default the grace window to off.)
7. Handle the error gracefully
If a refresh still fails, sign the user out locally and send them to your login page with a short message, instead of showing a broken page.
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Replit, v0 or whichever tool you build with:
Users get logged out with "AuthApiError: Invalid Refresh Token: Refresh Token Not Found". Fix only session handling:
1. Change every supabase.auth.signOut() call to signOut({ scope: 'local' }) unless it's an explicit "log out of all devices" button.
2. Upgrade @supabase/ssr and @supabase/supabase-js to the latest versions (ssr must be 0.8.1 or newer).
3. If this is Next.js: make the middleware match Supabase's current SSR docs, create the server client inside each request (never in module scope), and don't call getUser/getSession in both middleware and the API route for the same request.
4. Create the browser Supabase client once and reuse it. Register onAuthStateChange early.
5. If a refresh fails, sign out locally and redirect to /login with a friendly message.
List the files you changed. Don't change the database, RLS policies or UI design.How to confirm it's fixed
- Log in on two browsers with the same test account. Log out in one. The other should stay logged in.
- Lower the JWT expiry temporarily in a test project (Supabase doesn't recommend going below 5 minutes), keep a tab open past it, and check that the session refreshes without errors.
- Watch your server logs for a day for the error string.
How to find out sooner next time
Heedza won't catch this one directly: it never signs in, so it never holds a session. What it does is check on a schedule that your login and signup pages still load and show their forms, and send you a screenshot and a fix prompt when they don't. That's useful if a session fix like this one accidentally breaks the login page itself. The free scan also checks your homepage for exposed keys and missing security headers.
Let Heedza check this for you. Heedza’s log-in check keeps opening your live log-in page and tells you if it stops opening or loses its form. It never logs in or fills in forms, so it can’t see errors that only show after that.
Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.