Início / RLS / Erro JWT expired (PGRST303) · Read in English
Supabase Auth · Correção de erro
Erro JWT expired (PGRST303) no Supabase
O token de acesso da requisição passou da validade, e a Data API responde 401 PGRST303 JWT expired. No navegador, o supabase-js renova sozinho. No servidor, ele só fica válido se algo renovar: no Next.js, é o proxy.ts (middleware.ts antes do Next 16) do @supabase/ssr, chamando supabase.auth.getClaims().
01 O que é o erro
resposta
{ "code": "PGRST303", "message": "JWT expired" }
Os tokens de acesso do Supabase duram pouco e vêm com um refresh token que gera um novo. O erro quer dizer que uma requisição saiu com um token velho que ninguém renovou. Não é problema de RLS: suas policies nem chegaram a rodar.
02 De onde vem
Código de servidor que lê a sessão sem renovar
Server Components do Next.js não podem gravar cookies, então não conseguem salvar um token renovado. Se nenhum proxy/middleware renovar antes, as queries do servidor saem com token vencido.
Token passado na mão
Um cliente criado com o header fixo Authorization: Bearer ..., ou um token guardado no seu próprio estado, para de funcionar quando vence: nada renova ele.
Aba que hibernou
Quando o notebook volta da hibernação, a primeira requisição pode sair antes da renovação. O supabase-js renova sozinho; tentar de novo depois da renovação funciona.
03 A correção no Next.js
Use o @supabase/ssr e renove no proxy, que roda antes das páginas e pode gravar cookies. Seguindo o guia do Supabase, o proxy cria um cliente de servidor e chama getClaims(), que renova um token vencido:
proxy.ts (middleware.ts antes do Next 16)
// dentro do proxy, depois de criar o cliente de servidor com os cookies da requisição await supabase.auth.getClaims(); // renova o token e grava os cookies novos
Não confie no getSession() no código do servidor: ele lê o cookie sem conferir. E crie o cliente do Supabase a cada requisição, a partir dos cookies, nunca de um token guardado.
04 Não confunda com
PGRST301: o token não pode ser decodificado ou é inválido (projeto errado, header malformado).- Sem sessão nenhuma: as requisições rodam como
anone policiesto authenticatednão devolvem nada. Veja o PGRST116 (em inglês).
Seu app está vazando agora?
Cole a URL do seu app no ar. O scanner lê a página e o JavaScript atrás de uma chave service_role do Supabase, o vazamento que passa por cima de todas as policies deste guia.
Pegue isso antes de ir pro ar
airlock-rls é um gate de CI que quebra o build quando uma tabela vai pro ar exposta ou uma policy é permissiva: o mesmo tipo de bug, pego no CI contra um banco com as suas migrations aplicadas, antes de chegar em produção.
Ou comece pelo nextjs-supabase-starter: auth + uma tabela com RLS + um teste de isolamento, pra começar de um padrão que já vem selado.
Baixe o cheat sheet de RLS do Supabase
As regras de ouro, as armadilhas que vazam em produção, policies corretas e o teste de isolamento, numa página só (em inglês).
FAQ
Devo aumentar a validade do JWT?
Isso esconde o sintoma e deixa um token roubado útil por mais tempo. Corrija a renovação: com o proxy funcionando, a validade deixa de importar pro usuário.
A chave service_role expira?
Ela não é uma sessão de usuário, então não há o que renovar. E ela ignora o RLS, então fica só no servidor, nunca como solução pra um token de usuário vencido.