JWT expired

A request carried a sign-in token past its expiry time, usually because code saved the token once instead of using the refreshed one.

API response and browser console
JWT expired

Also appears as

  • {"code":"PGRST303","details":null,"hint":null,"message":"JWT expired"}
  • {"code":"PGRST301","details":null,"hint":null,"message":"JWT expired"}
  • TokenExpiredError: jwt expired
  • JWTExpired: "exp" claim timestamp check failed

Prompt to fix it

Requests start failing with "JWT expired" after the app has been open for a while. Find every place that reads, stores, or sends the Supabase access token. Replace saved copies with a fresh token from supabase.auth.getSession() right before each request, use one shared Supabase client with auto refresh on, and if this is Next.js, add the @supabase/ssr session refresh middleware. Retry once after refreshing if a request still gets a 401. Don’t raise the JWT expiry time to hide the problem.

Paste into Layerwise or any AI coding tool.

What it means

A JWT (JSON Web Token) is the signed pass that proves who a request comes from. Supabase gives each signed-in user an access token that expires after one hour by default, plus a refresh token. supabase-js uses the refresh token to get a new access token shortly before the old one runs out.

So this error almost always means some code sent an old token. It read session.access_token once, kept it in state, localStorage, or a custom Authorization header, and went on sending it after the client had already swapped in a new one. That explains the usual pattern: everything works for an hour, then requests start failing.

Server-rendered apps have their own version. In Next.js, server components can’t write cookies, so the session cookie only gets refreshed if middleware does it on each request (proxy.ts in Next.js 16). Supabase’s @supabase/ssr guide includes that file; without it, server code keeps reading an expired token.

Common causes

  • The code saves session.access_token once and keeps sending it to an API or edge function.
  • A Supabase client is created with a fixed Authorization header, so it never picks up refreshed tokens.
  • A Next.js app reads the session on the server, and no middleware refreshes the cookie.
  • Your own backend checks tokens with a library like jsonwebtoken, and the frontend never fetches a new token.

How to fix it

  1. Search the code for access_token and remove saved copies.
  2. Right before each call that needs the token, get it from supabase.auth.getSession(), which refreshes an expired session first.
  3. Use one shared Supabase client with the default autoRefreshToken: true.
  4. In Next.js, add the session-refresh middleware from Supabase’s @supabase/ssr guide, and create server clients with createServerClient.
  5. If a request still returns 401 with this message, refresh the session once and retry.
  6. Treat a longer JWT expiry in Supabase’s settings as a last resort: it only delays the error, and a stolen token keeps working longer.

Example fix

async function callApi(path: string, body: unknown) {
  // getSession() refreshes the token first if it has expired.
  const { data } = await supabase.auth.getSession();

  return fetch(path, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${data.session?.access_token}`
    },
    body: JSON.stringify(body)
  });
}
All errors
GET https://api.example.com/items 401 (Unauthorized)

401 Unauthorized

The server couldn’t confirm who was calling: the request had no credentials, or its API key, session, or token was wrong or expired.

AuthSessionMissingError: Auth session missing!

AuthSessionMissingError: Auth session missing

A Supabase auth call needed a signed-in user, but the client making it had no session, often because it runs on the server without cookies.

new row violates row-level security policy for table "todos"

New row violates row-level security policy

Postgres refused to save a row because no row-level security policy on that table lets the current user write it.

Access blocked: This app's request is invalid You can't sign in because this app sent an invalid request. You can try again later, or contact the developer about this issue. Error 400: redirect_uri_mismatch

Error 400: redirect_uri_mismatch

Google stopped the sign-in because the return address your app sent isn’t on the OAuth client’s list of approved redirect URIs.

Build with an agent that checks its work

The Layerwise agent clicks through the live preview and reads the console, so it catches errors before you do.