Home / RLS / Fix JWT expired (PGRST303) · Em português

Supabase Auth · Error fix

Fix JWT expired (PGRST303) in Supabase

Supabase Auth · Next.js✓ code tested against a real database
TL;DR

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

CAUSE 1 · most common

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.

CAUSE 2

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.

CAUSE 3

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

◆ FREE · 30 SECONDS · NO SIGNUP

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.

◆ FREE · MIT · npm + GitHub Action

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.

npx airlock-rls→

Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.

FREE PDF

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.