Noções de segurança
Supabase "Invalid Refresh Token: Refresh Token Not Found": 3 causas
Supabase "Invalid Refresh Token: Refresh Token Not Found" desconecta seus usuários. Três causas, e como o log do Auth separa cada uma.

Em resumo
- Supabase "Invalid Refresh Token: Refresh Token Not Found" significa que o Supabase Auth não tem registro do refresh token que seu app enviou, então a sessão não pode ser renovada e o usuário é desconectado.
- Três coisas apagam esse registro: um logout (que no JavaScript encerra todos os dispositivos por padrão), um token já usado que chega uma segunda vez e um limite de sessão nas suas configurações do Auth.
- Parte disso é uma linha inofensiva de uma aba antiga. Outra parte desconecta todos os seus usuários todo dia. O log do Auth diz qual é, e cada causa tem sua própria correção.
Seus usuários escrevem para dizer que foram desconectados. Um estava no meio de um formulário, e outra tem certeza de que não apertou nada. Quando você testa o app, ele funciona. Aí você abre seus logs e encontra a mesma linha, de novo e de novo:
AuthApiError: Invalid Refresh Token: Refresh Token Not Found
Supabase "Invalid Refresh Token: Refresh Token Not Found" é o Supabase Auth, a parte do Supabase que faz as pessoas entrarem, dizendo que não tem registro do token que seu app enviou para manter uma sessão aberta. Sem nada para renovar, seu app desconecta o usuário.
Esta é a parte que é fácil entender ao contrário: esse erro é o último passo, e a causa veio antes. Quando o Supabase não encontra mais o token, a sessão a que ele pertencia já terminou. Quem a encerrou foi um logout, um token enviado duas vezes ou um limite que alguém definiu. Fazer o access token durar mais, ou desligar a verificação que encerra uma sessão, deixa a linha mais rara nos seus logs e deixa a causa exatamente onde estava.
O que significa "Invalid Refresh Token: Refresh Token Not Found"?
Significa que o Supabase Auth recebeu um refresh token do qual não tem registro, e por isso se recusou a renovar a sessão.
Todo usuário conectado tem dois tokens. O access token funciona como uma pulseira: prova quem ele é por um tempo, uma hora se você não mudou a configuração, e seu app a mostra em cada requisição. O refresh token é um bilhete. Antes que a pulseira expire, seu app leva o bilhete ao balcão, que é o Supabase Auth, e o troca por uma pulseira nova e um bilhete novo. O guia de sessões do Supabase diz que refresh tokens "never expire but can only be used once".
Refresh Token Not Found é o balcão procurando um bilhete e não encontrando
nada. O servidor responde com HTTP 400 e o código refresh_token_not_found, que
a
lista de códigos de erro do Auth
do Supabase descreve como "Session containing the refresh token not found." Se a
pulseira também expirou,
o cliente JavaScript do Supabase
remove a sessão do navegador e avisa seu app de que o usuário saiu. É nesse
momento que seu usuário dá de cara com a página de login.
Nenhum dos dois tokens é uma das chaves de API do seu projeto, que são outra
questão:
quais delas pertencem ao seu app.
Se a sua mensagem diz Invalid JWT, foi a pulseira que foi recusada, e não o
bilhete, e
esse erro tem uma página própria.
Se depois a pessoa não consegue entrar de novo e o formulário de login diz
Invalid login credentials,
essa mensagem cobre três pessoas diferentes.
Por que meus usuários do Supabase são desconectados?
Porque a sessão por trás do bilhete deles terminou, e a pulseira que eles ainda usam os manteve conectados até expirar. Esse atraso, de até uma hora com a configuração padrão, é o que faz as desconexões parecerem aleatórias: o usuário é jogado para fora um tempo depois de a sessão terminar.
O Supabase Auth tem quatro jeitos de dizer que um bilhete não vale, e cada um aponta para uma causa diferente. Estas são as mensagens exatas do código do próprio servidor do Supabase Auth:
| A mensagem no seu log | O código | O que aconteceu |
|---|---|---|
Invalid Refresh Token: Refresh Token Not Found | refresh_token_not_found | O balcão não tem registro deste bilhete. Um logout encerrou a sessão dele, ou ele foi trocado há tanto tempo que o registro sumiu. |
Invalid Refresh Token: Already Used | refresh_token_already_used | O bilhete foi trocado há mais de dez segundos e voltou. O Supabase encerra a sessão inteira quando isso acontece. |
Invalid Refresh Token: Session Expired, às vezes com (Inactivity) ou (Revoked by Newer Login) depois | session_expired | Um limite de sessão nas suas configurações do Auth a encerrou. |
Invalid Refresh Token: No Valid Session Found | session_not_found | A sessão a que o bilhete pertencia não existe mais. |
Por trás dessas mensagens estão três causas, segundo a própria documentação do Supabase e os relatos nos seus issue trackers, e cada uma tem sua correção. Se ninguém consegue entrar, ou o app inteiro está em branco, é outro problema, e o Supabase caiu, ou é o seu app? é por onde começar.
Um refresh token funciona uma vez
Toda vez que seu app renova uma sessão, ele entrega o bilhete e o balcão rasga o antigo. O Supabase chama isso de rotação de refresh tokens. O bilhete antigo continua funcionando por mais dez segundos, o que o guia de sessões chama de reuse interval, para que um servidor e um navegador renovando quase no mesmo momento não derrubem um ao outro. Um cliente que trocou o bilhete e perdeu a resposta numa conexão ruim também está coberto: ele recebe o bilhete atual de volta.
Fora desses dois casos, um bilhete antigo que volta conta como sinal de alerta,
porque um token que aparece duas vezes é o que um token roubado parece. O guia
diz que a sessão inteira então "is regarded as terminated and all refresh tokens
belonging to it are marked as revoked". A resposta dessa vez é Already Used.
Mais um passo transforma Already Used em Not Found. O servidor do Supabase
Auth
apaga os registros dos bilhetes rasgados
24 horas depois de serem revogados. Então o bilhete vencido que hoje recebe
Already Used amanhã recebe Refresh Token Not Found, porque o balcão não tem
mais o registro para comparar.
Causa 1: alguém saiu, neste dispositivo ou em outro
Um logout apaga a sessão do lado do balcão, e cada bilhete dela vira um que o
balcão não encontra. No cliente JavaScript do Supabase, signOut() sem nada
dentro dos parênteses encerra todas as sessões daquele usuário, em todos os
dispositivos. O guia de logout
do Supabase diz isso com todas as letras: com esse padrão, as outras sessões
falham com AuthApiError: Invalid Refresh Token: Refresh Token Not Found na
próxima vez que tentam se renovar, "even though no one signed out on that
device."
Então um usuário que sai no celular também é desconectado no notebook, até uma hora depois, quando a pulseira do notebook expira e o bilhete dele volta como desconhecido. Do ponto de vista dele, seu app o jogou para fora sem motivo.
Se cada dispositivo deve continuar conectado por conta própria, a chamada de logout precisa encerrar só a sessão de onde ela partiu:
await supabase.auth.signOut({ scope: 'local' })
Peça ao seu builder para usar essa forma em todo lugar onde seu app desconecta as pessoas. O outro jeito comum de encerrar uma sessão é uma troca de senha, que o guia de sessões do Supabase lista entre as coisas que encerram uma.
Causa 2: o bilhete novo nunca chegou ao navegador
Se seu app gera as páginas num servidor, como faz um app Next.js, esse servidor
também renova sessões. Ele lê o bilhete de um cookie, troca no balcão e precisa
devolver o bilhete novo ao navegador num cabeçalho Set-Cookie da sua resposta.
Quando esse cookie não chega, o navegador fica com o bilhete antigo, e a próxima
requisição apresenta um bilhete que o servidor já trocou. Em até dez segundos,
isso passa. Depois disso é Already Used e a sessão termina, e um dia depois o
mesmo bilhete é Not Found.
O
guia do Supabase para criar um cliente de servidor
aponta o erro que causa isso num app Next.js. O código que renova a sessão, que o
Next.js 16 chama de Proxy e as versões anteriores de middleware, precisa
devolver a resposta em que os cookies novos foram escritos: "An earlier response
doesn't carry the refreshed cookies, so the user is signed out on the next
request." O mesmo guia avisa que no Next.js 15 e anteriores um arquivo chamado
proxy.ts nunca é executado, "so sessions never refresh and users get signed
out". Um assistente trabalhando com a documentação mais nova pode escrever
exatamente esse arquivo num app mais antigo.
O guia do Supabase também diz por que esse caso é difícil de achar: "it looks
like users being signed out at random rather than an error in your code." Os
relatos soam igual. Um desenvolvedor escreveu no
issue tracker do @supabase/ssr
"I'm inadvertently logged out of my web application every day", e o engenheiro
do Supabase que respondeu apontou o jeito como o middleware repassava a
resposta. O
relato de um app de iOS
descreve a desconexão chegando "2-3 days after logging in", e foi fechado depois
que atualizações na biblioteca Swift do Supabase reduziram os relatos.
Para conferir, abra as ferramentas de desenvolvedor do navegador numa página que
renovou a sessão e olhe a resposta na aba Network. Ela deve trazer um
Set-Cookie com o token novo, e a requisição seguinte deve mandar esse token de
volta.
Causa 3: um limite de sessão nas suas configurações do Auth
A partir do plano Pro, o Supabase pode encerrar sessões de propósito, pela página Sessions das configurações do Auth do seu projeto. São três limites, e o guia de sessões descreve cada um:
- Sessões com tempo limitado terminam depois de um tempo fixo, por mais ativo que o usuário esteja.
- Um tempo limite de inatividade encerra uma sessão que não é renovada há um tempo.
- Uma sessão por usuário mantém só o login mais recente, então entrar pelo celular encerra a sessão no notebook.
Esses dizem isso na própria mensagem, Session Expired, com (Inactivity) ou
(Revoked by Newer Login) depois, no segundo e no terceiro. O guia acrescenta
que sessões encerradas são apagadas 24 horas depois, então um bilhete de uma
delas que volta depois desse prazo recebe Not Found no lugar.
Se você configurou um desses, as desconexões são a configuração fazendo o seu trabalho. Se não era a intenção, ou se um template ou um assistente ligou isso por você, a página mostra qual limite está ativo.
O caso inofensivo
Um bilhete de uma sessão que já terminou, voltando de uma aba antiga ou de um
navegador que ninguém abria havia semanas, também recebe
Refresh Token Not Found. A pessoa por trás já tinha sido desconectada. O
navegador dela perguntou uma vez, o Supabase disse não, e ela vê sua página de
login, que é o que teria visto de qualquer jeito.
No log, aparece assim: uma linha para um usuário, muitas vezes logo depois de esse usuário sair em algum lugar, e depois nada mais dele. Linhas assim são o erro fazendo o seu trabalho. As que valem investigar são as mesmas pessoas de novo e de novo, ou um monte de pessoas diferentes no mesmo dia.
Como ler seus logs do Auth para isso
Abra Logs no painel do Supabase, escolha o tipo de log Auth e procure
refresh token nas mensagens de evento. Selecione uma linha para ver os
detalhes, incluindo o ID do usuário, e o filtro User então reduz a lista a
essa pessoa. Até onde os logs voltam depende do seu plano, então leia enquanto
as reclamações são recentes.
Depois leia o que se repete:
| O que você vê no log do Auth | Para onde aponta |
|---|---|
Um Refresh Token Not Found para um usuário, e nada depois | Uma aba antiga ou um navegador que voltou. Inofensivo. |
Refresh Token Not Found para um usuário logo depois de ele sair em outro lugar | Causa 1, um logout que encerrou todos os dispositivos |
Already Used, e um dia depois Refresh Token Not Found, para os mesmos usuários, de novo e de novo | Causa 2, bilhetes novos que não chegam ao navegador |
Session Expired, com um motivo entre parênteses | Causa 3, um limite de sessão |
Você também pode perguntar ao banco de dados com que frequência as pessoas
entram. Cada login acrescenta uma linha em auth.sessions, e um logout a apaga.
Cole isto no 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;
Um usuário com muito mais sessões do que dispositivos entrou de novo e de novo sem nunca sair, que é o que faz alguém que seu app joga para fora o tempo todo.
O que não mudar enquanto você corrige
Suas regras de Row Level Security, a detecção de reutilização e a duração do access token. As três parecem uma correção e deixam a causa onde está, e afrouxar a primeira pode abrir seus dados para estranhos.
Deixe suas regras de Row Level Security em paz. Row Level Security é o
conjunto de regras que decide quais linhas cada visitante recebe. Enquanto os
usuários estão sendo jogados para fora, toda tela cuja regra lê auth.uid() fica
vazia para eles, porque segundo a
documentação do Supabaseauth.uid() retorna null quando a sessão expirou. A correção tentadora é
afrouxar essa regra até a tela encher de novo, e aí ela enche também para
estranhos. Se alguém já fez isso, nossa verificação gratuita pergunta a cada
tabela que seu app nomeia quantas linhas um visitante sem login receberia, lê o
número sem baixar uma única linha e leva cerca de 20 segundos, sem conta:
verifique seu app.
Deixe a detecção de reutilização ligada. O Supabase permite desligá-la, nas
configurações avançadas da página Sessions, e chama isso de "generally not
recommended". Com ela desligada, a causa 2 para de produzir Already Used, e o
bilhete novo continua sem chegar ao navegador.
Mantenha o access token em uma hora. Uma pulseira mais longa adia a desconexão e deixa a causa onde está. O guia do Supabase diz que definir mais de uma hora é "generally discouraged for security reasons".
Mantendo os logins funcionando depois da correção
Duas coisas ainda podem dar errado quando as desconexões param: uma mudança posterior as traz de volta, e uma regra afrouxada no caminho deixa uma tabela aberta para estranhos.
Reeve Monitor continua verificando de fora:
- se seu app está no ar, verificado a cada minuto, para que uma onda de mensagens de "fui desconectado" não seja confundida com uma queda
- a verificação de segurança completa a cada hora, em até três apps
- uma mensagem quando um resultado muda, para que uma regra afrouxada durante uma correção não espere você olhar
- um relatório mensal, em linguagem simples, do que ele viu
Reeve Care guarda uma cópia do seu banco de dados Supabase:
- uma cópia criptografada em uma programação, no armazenamento próprio da Reeve na UE, fora da sua conta do Supabase
- o schema auth onde vivem as contas dos seus usuários, para que elas voltem com todo o resto
- cada cópia verificada antes de contar, contando as linhas de cada tabela
- seus arquivos enviados também, assim que você conectar uma credencial do Storage
- uma restauração com um clique, com uma cópia de segurança do estado atual feita antes
- tudo o que o Monitor faz
O que fazer agora
O que fazer
- Procure
refresh tokennos seus logs do Auth e veja qual mensagem se repete, e com quem. - Se um logout em um dispositivo encerra os outros, troque para
signOut({ scope: 'local' }). - Se seu app gera páginas num servidor, garanta que a resposta que renovou uma sessão é a que chega ao navegador, e procure o
Set-Cookiedela na aba Network. - Veja na página Sessions das configurações do Auth se há um tempo limite, um limite de inatividade ou uma sessão por usuário que ninguém quis ligar.
- Deixe Row Level Security, a detecção de reutilização e a duração do access token como estão enquanto corrige.
- Trate um
Not Foundpor usuário como ruído, e os mesmos usuários todo dia como um bug.
Comece pelo log do Auth, porque a mensagem que se repete diz qual das três causas você tem. Depois, quando as pessoas continuarem conectadas, verifique seu app para conferir que nada foi afrouxado no caminho.
Perguntas frequentes
Por que meus usuários do Supabase são desconectados do nada?
Porque a sessão por trás do refresh token deles terminou, e o access token que eles ainda têm os mantém conectados por até uma hora, então a desconexão chega depois da causa. Os motivos mais comuns são um logout em outro dispositivo, um token renovado que nunca chegou ao navegador e um limite de sessão nas suas configurações do Auth. O log do Auth mostra qual é.
O que é um refresh token?
É a metade duradoura de uma sessão do Supabase. O access token prova quem é o usuário por uma hora por padrão, e antes que ele expire seu app troca o refresh token por um par novo. Cada refresh token funciona uma vez, e o antigo para de funcionar dez segundos depois da troca.
"Refresh Token Not Found" é sinal de um ataque?
Sozinho, não. Significa que o Supabase Auth recusou um token do qual não tem registro, que é a resposta certa. A proteção contra um token roubado é a detecção de reutilização, que encerra uma sessão quando um token antigo volta, e o Supabase recomenda deixá-la ligada.
Por que isso só acontece em produção?
Porque a maioria das causas precisa de tempo ou de um segundo dispositivo. Um token vencido é recusado como já usado no primeiro dia e como não encontrado quando o registro dele é apagado um dia depois, e uma sessão que você abre enquanto constrói raramente dura tanto. Usuários de verdade também entram por vários dispositivos, e é aí que aparece um logout que encerra todas as sessões.
Quanto tempo dura uma sessão do Supabase?
Por padrão, até o usuário sair. O access token dura uma hora se você não mudar, e o refresh token nunca expira, mas só funciona uma vez. A partir do plano Pro, você pode limitar as sessões por tempo total, por inatividade ou a uma por usuário, na página Sessions das suas configurações do Auth.