Skip to content

Security basics

Supabase "Invalid Refresh Token: Refresh Token Not Found": 3 causes

Supabase "Invalid Refresh Token: Refresh Token Not Found" logs your users out. Three causes, and how the auth log tells them apart.

Vlad Tkachenko13 min read
A ticket torn in two, its left half lit and its right half drained and falling away, in front of a row of whole tickets.

In short

  • Supabase "Invalid Refresh Token: Refresh Token Not Found" means Supabase Auth has no record of the refresh token your app sent, so the session cannot be renewed and the user is signed out.
  • Three things remove that record: a sign-out (which in JavaScript ends every device by default), a used token sent a second time, and a session limit in your Auth settings.
  • Some of it is a harmless line from an old tab. Some of it signs every user out every day. The auth log tells you which, and each cause has its own fix.

Your users write in to say they were signed out. One was halfway through a form, and another is sure they never pressed anything. When you try the app yourself, it works. Then you open your logs and find the same line, again and again:

AuthApiError: Invalid Refresh Token: Refresh Token Not Found

Supabase "Invalid Refresh Token: Refresh Token Not Found" is Supabase Auth, the part of Supabase that signs people in, saying it has no record of the token your app sent to keep a session going. With nothing left to renew, your app signs the user out.

Here is the part that is easy to get backwards: this error is the last step, and the cause came earlier. By the time Supabase cannot find the token, the session it belonged to has already ended. A sign-out ended it, or a token sent twice, or a limit somebody set. Making the access token last longer, or turning off the check that ends a session, makes the line rarer in your logs and leaves the cause exactly where it was.

What does "Invalid Refresh Token: Refresh Token Not Found" mean?

It means Supabase Auth was handed a refresh token it has no record of, so it refused to renew the session.

Every signed-in user holds two tokens. The access token works like a wristband: it proves who they are for a short while, an hour unless you have changed the setting, and your app shows it with every request. The refresh token is a ticket. Before the wristband runs out, your app takes the ticket to the desk, which is Supabase Auth, and trades it for a new wristband and a new ticket. Supabase's guide to sessions says refresh tokens "never expire but can only be used once".

Refresh Token Not Found is the desk looking a ticket up and finding nothing. The server answers with HTTP 400 and the code refresh_token_not_found, which Supabase's list of Auth error codes describes as "Session containing the refresh token not found." If the wristband has run out as well, Supabase's JavaScript client then removes the session from the browser and tells your app the user signed out. That is the moment your user meets the sign-in page.

Neither token is one of your project's API keys, which are a separate question: which of those belong in your app. If your message says Invalid JWT instead, the wristband was refused rather than the ticket, and that error has its own page. If they cannot get back in afterwards and the sign-in form says Invalid login credentials, that message covers three different people.

Why do my Supabase users get logged out?

Because the session behind their ticket ended, and the wristband they still wear kept them signed in until it ran out. That delay, up to an hour with the default setting, is why the logouts look random: the user is thrown out a while after the session ended.

Supabase Auth has four ways of saying a ticket is no good, and each points at a different cause. These are the exact messages from the Supabase Auth server's own code:

The message in your logThe codeWhat happened
Invalid Refresh Token: Refresh Token Not Foundrefresh_token_not_foundThe desk has no record of this ticket. A sign-out ended its session, or it was traded in long enough ago that its record is gone.
Invalid Refresh Token: Already Usedrefresh_token_already_usedThe ticket was traded in more than ten seconds ago and has come back. Supabase ends the whole session when this happens.
Invalid Refresh Token: Session Expired, sometimes with (Inactivity) or (Revoked by Newer Login)session_expiredA session limit in your Auth settings ended it.
Invalid Refresh Token: No Valid Session Foundsession_not_foundThe session the ticket belonged to no longer exists.

Three causes sit behind these messages in Supabase's own documentation and in the reports on its issue trackers, and each has its own fix. If nobody can sign in at all, or the whole app is blank, you are looking at something else, and is Supabase down, or is it your app is where to start.

A refresh token works once

Each time your app renews a session, it hands the ticket over and the desk tears the old one up. Supabase calls this refresh token rotation. The old ticket keeps working for ten more seconds, which the sessions guide calls the reuse interval, so that a server and a browser renewing at nearly the same moment do not knock each other out. A client that traded its ticket and lost the answer on a bad connection is also covered: it gets the current ticket back.

Outside those two cases, an old ticket coming back counts as a warning sign, because a token turning up twice is what a stolen one looks like. The guide says the whole session "is regarded as terminated and all refresh tokens belonging to it are marked as revoked". The answer that time is Already Used.

One more step turns Already Used into Not Found. The Supabase Auth server clears out the records of torn-up tickets 24 hours after they were revoked. So the stale ticket that got Already Used today gets Refresh Token Not Found tomorrow, because the desk no longer has the record to compare it with.

One ticket at four moments. Inside ten seconds it still works. After that it is refused as already used, and once its record is cleared a day later, the desk cannot find it at all.

Cause 1: somebody signed out, on this device or another

A sign-out deletes the session on the desk's side, and every ticket from it becomes one the desk cannot find. In Supabase's JavaScript client, signOut() with nothing inside the brackets ends every session that user has, on every device. Supabase's guide to signing out says so directly: with that default, the other sessions "fail with AuthApiError: Invalid Refresh Token: Refresh Token Not Found the next time they try to refresh, even though no one signed out on that device."

So a user who signs out on their phone is signed out of their laptop as well, up to an hour later, when the laptop's wristband runs out and its ticket comes back unknown. From where they sit, your app threw them out for no reason.

If each device should stay signed in on its own, the sign-out call has to end only the session it was made from:

await supabase.auth.signOut({ scope: 'local' })

Ask your builder to use that form everywhere your app signs people out. The other ordinary way to end a session is a password change, which Supabase's sessions guide lists among the things that terminate one.

With no scope, one sign-out ends the session on every device. With scope local, it ends only the one in your hand.

Cause 2: the new ticket never reached the browser

If your app renders pages on a server, as a Next.js app does, that server renews sessions too. It reads the ticket from a cookie, trades it at the desk, and has to send the new ticket back to the browser in a Set-Cookie header on its response. When that cookie does not arrive, the browser keeps the old ticket, and the next request presents a ticket the server has already traded. Inside ten seconds that passes. After that it is Already Used and the session ends, and a day later the same ticket is Not Found.

Supabase's guide to creating a server client names the mistake that does this in a Next.js app. The code that renews the session, which Next.js 16 calls the Proxy and earlier versions call middleware, must return the response the new cookies were written to: "An earlier response doesn't carry the refreshed cookies, so the user is signed out on the next request." The same guide warns that on Next.js 15 and earlier a file named proxy.ts is never run at all, "so sessions never refresh and users get signed out". An assistant working from the newest documentation can write exactly that file into an older app.

Supabase's guide also says why this one is hard to find: "it looks like users being signed out at random rather than an error in your code." The reports read the same way. A developer on the @supabase/ssr issue tracker wrote "I'm inadvertently logged out of my web application every day", and the Supabase engineer who answered pointed at how the middleware passed its response along. An iOS app's report describes the logout coming "2-3 days after logging in", and was closed after updates to Supabase's Swift library brought the reports down.

The server renewed the session, and the new ticket never made it back. The browser is still holding the one that was traded.

To check, open your browser's developer tools on a page that renewed the session and look at the response in the Network tab. It should carry a Set-Cookie with the new token, and the next request should send that token back.

Cause 3: a session limit in your Auth settings

On Pro plans and up, Supabase can end sessions on purpose, from the Sessions page of your project's Auth settings. There are three limits, and the sessions guide describes each:

  • Time-boxed sessions end after a fixed time, however active the user is.
  • An inactivity timeout ends a session that has not been renewed for a while.
  • Single session per user keeps only the most recent sign-in, so signing in on a phone ends the session on the laptop.

These say so in their own message, Session Expired, with (Inactivity) or (Revoked by Newer Login) after it for the second and third. The guide adds that ended sessions are deleted 24 hours later, so a ticket from one that comes back after that gets Not Found instead.

If you set one of these, the logouts are the setting working. If you did not mean to, or a template or an assistant did it for you, the page shows which limit is on.

The one that is harmless

A ticket from a session that already ended, coming back from an old tab or a browser nobody opened for weeks, gets Refresh Token Not Found too. The person behind it had already been signed out. Their browser asked once, Supabase said no, and they see your sign-in page, which is what they would have seen anyway.

It looks like this in a log: one line for one user, often soon after that user signed out somewhere, and then nothing more for them. Lines like that are the error doing its job. The ones to chase are the same people over and over, or a burst of different people on the same day.

How to read your auth logs for this

Open Logs in your Supabase dashboard, choose the Auth log type, and search the event messages for refresh token. Select a row to see its details, including the user's ID, and the User filter then narrows the list to that one person. How far back the logs go depends on your plan, so read them while the complaints are recent.

Then read what repeats:

What you see in the Auth logWhat it points to
One Refresh Token Not Found for a user, and nothing after itAn old tab or a returning browser. Harmless.
Refresh Token Not Found for a user soon after they signed out elsewhereCause 1, a sign-out that ended every device
Already Used, then Refresh Token Not Found a day later, for the same users, again and againCause 2, new tickets not reaching the browser
Session Expired, with a reason in bracketsCause 3, a session limit

You can also ask the database how often people are signing in. Each sign-in adds a row to auth.sessions, and a sign-out deletes it. Paste this into the SQL Editor:

select user_id, count(*) as sessions, max(created_at) as latest
from auth.sessions
where created_at > now() - interval '14 days'
group by user_id
order by sessions desc
limit 20;

A user with far more sessions than they have devices has been signing in again and again without ever signing out, which is what somebody does when your app keeps throwing them out.

What not to change while you fix it

Your Row Level Security rules, reuse detection and the length of the access token. Each of the three looks like a fix and leaves the cause in place, and loosening the first can open your data to strangers.

Leave your Row Level Security rules alone. Row Level Security is the set of rules that decides which rows each visitor gets. While users are being thrown out, every screen whose rule reads auth.uid() comes up empty for them, because Supabase's documentation says auth.uid() returns null when the session has expired. The tempting fix is to loosen that rule until the screen fills up again, and then it fills up for strangers as well. If somebody has already done it, our free scan asks each table your app names how many rows a visitor with no login would get, reads the count without fetching a single row, and takes about 20 seconds with no account: scan your app.

Leave reuse detection on. Supabase lets you turn it off, under the advanced settings on the Sessions page, and calls doing so "generally not recommended". With it off, cause 2 stops producing Already Used and the new ticket still never reaches the browser.

Keep the access token at an hour. A longer wristband delays the logout and leaves its cause in place. Supabase's guide says that setting it over an hour is "generally discouraged for security reasons".

Keeping sign-ins working after the fix

Two things can still go wrong once the logouts stop: a later change brings them back, and a rule loosened along the way leaves a table open to strangers.

Reeve Monitor keeps checking from outside:

  • whether your app is up, checked every minute, so a wave of "I got logged out" messages is not mistaken for an outage
  • the full security check every hour, on up to three apps
  • a message when a result changes, so a rule loosened during a fix does not wait for you to look
  • a monthly report, in plain language, of what it saw

Reeve Care keeps a copy of your Supabase database:

  • an encrypted copy on a schedule, on Reeve's own storage in the EU, outside your Supabase account
  • the auth schema your users' accounts live in, so they come back with everything else
  • each copy verified before it counts, by counting the rows in every table
  • your uploaded files as well, once you connect a Storage credential
  • a one-click restore, with a safety copy of the current state taken first
  • everything Monitor does

What to do now

What to do

  • Search your Auth logs for refresh token and read which message repeats, and for whom.
  • If a sign-out on one device ends the others, change it to signOut({ scope: 'local' }).
  • If your app renders on a server, make sure the response that renewed a session is the one that reaches the browser, and check for its Set-Cookie in the Network tab.
  • Look at the Sessions page of your Auth settings for a time box, an inactivity timeout or single session per user that nobody meant to turn on.
  • Leave Row Level Security, reuse detection and the access token's length as they are while you fix it.
  • Treat one Not Found per user as noise, and the same users every day as a bug.

Start with the Auth log, because the message that repeats tells you which of the three causes you have. Then, once people stay signed in, scan your app to check that nothing was loosened along the way.

FAQ

Why do my Supabase users get logged out randomly?

Because the session behind their refresh token ended, and the access token they still hold keeps them signed in for up to an hour, so the logout arrives later than its cause. The usual reasons are a sign-out on another device, a renewed token that never reached the browser, and a session limit in your Auth settings. The auth log shows which one.

What is a refresh token?

It is the long-lived half of a Supabase session. The access token proves who the user is for an hour by default, and before it runs out your app trades the refresh token for a new pair. Each refresh token works once, and the old one stops working ten seconds after it is traded.

Is "Refresh Token Not Found" a sign of an attack?

On its own, no. It means Supabase Auth refused a token it has no record of, which is the correct answer. The protection against a stolen token is reuse detection, which ends a session when an old token comes back, and Supabase recommends leaving it switched on.

Why does it only happen in production?

Because most of the causes need time or a second device. A stale token is refused as already used on the first day and as not found once its record is cleared a day later, and a session you sign into while building rarely lives that long. Real users also sign in on several devices, which is where a sign-out that ends every session shows up.

How long does a Supabase session last?

Until the user signs out, by default. The access token lasts an hour unless you change it, and the refresh token never expires but works only once. On Pro plans and up you can limit sessions by total time, by inactivity, or to one per user, on the Sessions page of your Auth settings.

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.