Supabase: JWT expired after about an hour
JWT expired
In short: The access token in the request is older than its expiry (about an hour) and nothing refreshed it before the next API call.
Why it happens
Supabase access tokens expire after about 60 minutes. supabase-js in the browser will refresh them when you use one long-lived client and let it manage the session (persistSession: true, which is the default). The token expires in production when the app copies access_token into localStorage or a React variable and sends that same string as Authorization: Bearer … for the rest of the day; when it creates a new createClient() on every request without sharing the session; or when a server or Edge Function uses a cookie or header that was never refreshed (no middleware / @supabase/ssr refresh). The API then returns JWT expired (often code PGRST301) and the user looks logged out.
How to fix it
- Use a single browser client (a module-level
createClientorcreateBrowserClient) and callsupabase.from(...)on it. Don't storesession.access_tokenyourself and attach it tofetchhours later. If you need a token, read it at call time withconst { data: { session } } = await supabase.auth.getSession()and treat it as short-lived. - On Next.js App Router, use
@supabase/ssrwithcreateBrowserClientin Client Components andcreateServerClientplus the session-refresh middleware (orproxy.ts) documented by Supabase, so cookies are written back before they expire. - Check you did not set
persistSession: falseorautoRefreshToken: falseunless you have another refresh path. - If an Edge Function or webhook calls Supabase, don't pass the user's expired browser JWT. Use the function's service-role key on the server, or verify the user and mint a fresh server-side call.
- To confirm, sign in, wait for a request after 60+ minutes of idle, and watch the Network tab: you should see a token refresh, then the data request succeeding. If every request still carries the same Bearer token, the client is not refreshing.
Example
import { createClient } from "@supabase/supabase-js";
// One client for the browser tab. Don't create this inside render or a loop.
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
);
Copy-paste fix prompt
I'm getting this error in my app: JWT expired [Paste the full error, stack trace, and the file it points to here] Likely cause: The access token in the request is older than its expiry (about an hour) and nothing refreshed it before the next API call. Task: Requests fail with JWT expired after about an hour. Find where the access token is cached or where a new Supabase client is created per call, switch to one client that refreshes the session (and SSR middleware if this is Next.js), and stop attaching a stale Bearer token. Don't disable expiry or put the service-role key in the browser. Rules: - First explain the root cause in 2 sentences and list the files you'll change. - Make the smallest change that fixes it. Don't refactor unrelated code. - Don't disable security or checks (RLS, auth, signature verification, type checking, CORS allow-all) to make the error disappear. - Never print or hard-code secret keys. - Finish by telling me exactly how to verify the fix.
Common with: Cursor, Lovable, Bolt, v0. Last reviewed 2026-11-15.