Skip to content

Supabase API errorSupabase: “JWT expired”

Queries that worked a while ago start failing with 401 and JWT expired (PGRST303, or PGRST301 on older setups). Supabase access tokens are short-lived, and the client is supposed to refresh them in the background. Something stopped that: a token saved and reused, a header set once, or an app that was asleep when the refresh was due.

Short answer

Let supabase-js manage the session: don’t copy the access token into your own headers or storage. In React Native, start and stop auto refresh with AppState so the token refreshes when the app returns to the foreground. On the server, create a client per request from the user’s cookies instead of reusing a token. Raising the JWT expiry only hides the bug.

Why the token wasn’t refreshed, and the fix

  1. 01

    A token copied into a header or variable

    “Access tokens are designed to be short lived, usually between 5 minutes and 1 hour while refresh tokens never expire but can only be used once.” Supabase, User sessions

    Creating the client with a fixed Authorization header, or passing an access token you saved earlier, sends the same token after it expires. Use the client’s own session, or read session.access_token fresh each time you need it.

  2. 02

    A React Native app returning from the background

    “You should hook into your platform's foreground indication mechanism and call these methods appropriately to conserve resources.” Supabase, startAutoRefresh

    On mobile the OS can pause your app’s JavaScript while it is in the background, so a refresh due during that time is missed. Supabase’s docs say to hook into the platform’s foreground signal: start auto refresh when the app is active and stop it in the background.

    import { AppState } from 'react-native';
    AppState.addEventListener('change', (state) => {
      if (state === 'active') supabase.auth.startAutoRefresh();
      else supabase.auth.stopAutoRefresh();
    });
  3. 03

    A browser tab in the background

    “On browsers the refresh process works only when the tab/window is in the foreground to conserve resources as well as prevent race conditions and flooding auth with requests.” Supabase, startAutoRefresh

    In browsers, the refresh runs only while the tab is visible. Requests from a background tab, a worker or a long-running loop can go out with an old token. Refresh when the tab becomes visible again, before you send requests.

  4. 04

    Server code reusing one user token

    A server, cron job or Edge Function that stores a user’s access token fails once it expires. Create a client per request from the incoming cookies or Authorization header. For background jobs that act on their own, use a server-side secret key and scope the queries yourself, since that key bypasses RLS.

  5. 05

    What PGRST303 and PGRST301 mean

    “JWT claims validation or parsing failed.” Supabase, PostgREST error codes (PGRST303)

    PostgREST returns 401 with the message JWT expired when the token’s exp claim is in the past. Current PostgREST reports it as PGRST303; versions before 13 reported PGRST301. Either way the fix is a fresh token, not a database change.

Checked against the vendor’s docs on : Supabase docs: User sessions; Supabase reference: startAutoRefresh; Supabase docs: PostgREST error codes.

Rather hand it over?

Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.

Related: Supabase Invalid Refresh Token logouts · Supabase JWSError JWSInvalidSignature · Supabase logs users out on refresh · Supabase Email link is invalid or has expired

Questions

More on this error

Should I make the JWT expiry longer?

Supabase recommends the default of 1 hour for most apps and discourages going higher for security reasons. A longer expiry only delays the error if refresh is broken.

Why does it fail right after the device wakes up?

The token expired while the device slept, and the first request went out before the refresh. On mobile, start auto refresh when the app becomes active; in browsers, wait for the session to refresh before sending queries.

Can you fix it for me?

If one bug is the whole problem, the App Fix Ticket covers it: a written diagnosis first, then the fix. If the app has more going on, the Launch Check is a written check of the whole app. Each issue found is rated critical, high, medium or low, with where it is and how it was confirmed. No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.

A different error?

Describe it and get a written quote

Send the error and what you were doing. You get a written scope, a fixed price and a timeline; Simon replies by email, usually within one business day.