Home / RLS / Fix JWT expired (PGRST303) · Em português
Supabase Auth · Error fix
Fix JWT expired (PGRST303) in Supabase
The access token in the request is past its expiry, so the Data API answers 401 PGRST303 JWT expired. In the browser, supabase-js refreshes it for you. On the server it only stays fresh if something refreshes it: in Next.js that's the proxy.ts (middleware.ts before Next 16) from @supabase/ssr, calling supabase.auth.getClaims().
01 What the error is
response
{ "code": "PGRST303", "message": "JWT expired" }
Supabase access tokens are short-lived and come with a refresh token that gets a new one. The error means a request went out with an old access token that nobody refreshed. It isn't an RLS problem; your policies never ran.
02 Where it comes from
Server code that reads the session without refreshing it
Next.js Server Components can't write cookies, so they can't save a refreshed token. If no proxy/middleware refreshes it first, server-side queries go out with an expired token.
A token passed around by hand
A client built with a fixed Authorization: Bearer ... header, or a token stored in your own state, stops working when it expires: nothing refreshes it.
A tab that slept
After a laptop wakes up, the first request can race the refresh. supabase-js refreshes on its own; a retry after the refresh succeeds.
03 The fix in Next.js
Use @supabase/ssr and refresh in the proxy, which runs before your pages and can write cookies. Following Supabase's guide, the proxy creates a server client and calls getClaims(), which refreshes an expired token:
proxy.ts (middleware.ts before Next 16)
// inside the proxy, after creating the server client with the request cookies await supabase.auth.getClaims(); // refreshes the token and writes the new cookies
Don't rely on getSession() in server code: it reads the cookie without checking it. And create the Supabase client per request from the cookies, never from a stored token.
04 Not the same as these
PGRST301: the token can't be decoded or is invalid (wrong project, malformed header).- No session at all: requests run as
anonand policiesto authenticatedreturn nothing. See PGRST116.
Is your app leaking right now?
Paste your live URL. The scanner reads your page and its JavaScript for a Supabase service_role key, the one leak that skips every policy in this guide.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive: the same class of bug, caught in CI against a database with your migrations applied, before it reaches prod.
Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Should I make the JWT expiry longer?
It hides the symptom and makes a stolen token useful for longer. Fix the refresh instead: with the proxy in place, the expiry length stops mattering to users.
Does the service_role key expire?
It isn't a user session, so there's nothing to refresh. It also bypasses RLS, so it belongs on the server only, never as a fix for an expired user token.