All answers

Supabase

How do I stop "JWT expired" errors with Supabase in React Native?

Supabase access tokens last 1 hour by default. Create the client with persistSession and autoRefreshToken, store the session in AsyncStorage, start and stop auto refresh with AppState, and get the token from getSession() each time instead of saving it yourself.

Updated October 31, 2026

If your app works fine for a while and then every Supabase request fails with JWT expired, nothing is wrong with your users’ accounts. The access token that proves who’s signed in is short-lived on purpose, and the fix is making sure your app always uses a fresh one.

Why it happens

A Supabase session has two tokens. The access token, a JWT, goes with every request and expires after 1 hour by default. The refresh token gets you a new access token, and refresh tokens never expire, though each one can only be used once. That swap happens automatically inside the Supabase client, so when you see JWT expired, look for a request that went around it in one of these ways:

  • The access token was copied at sign-in into state, a variable, or a hand-written Authorization header, and that copy kept going out after it expired.
  • The client wasn’t set up to store and refresh the session, which is what persistSession, autoRefreshToken, and a storage adapter do.
  • The app came back from the background, where refreshing had stopped, and a request went out with the old token before a refresh ran.

How to fix it

  1. Install the client and storage:

    npx expo install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
  2. Create one client, and tie auto refresh to the app being in the foreground. This is the setup from Supabase’s React Native quickstart, where the AppState listener is registered just once:

    import "react-native-url-polyfill/auto";
    import AsyncStorage from "@react-native-async-storage/async-storage";
    import { createClient } from "@supabase/supabase-js";
    import { AppState, Platform } from "react-native";
    
    export const supabase = createClient(
      process.env.EXPO_PUBLIC_SUPABASE_URL!,
      process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
      {
        auth: {
          ...(Platform.OS !== "web" ? { storage: AsyncStorage } : {}),
          autoRefreshToken: true,
          persistSession: true,
          detectSessionInUrl: false,
        },
      },
    );
    
    if (Platform.OS !== "web") {
      AppState.addEventListener("change", (state) => {
        if (state === "active") supabase.auth.startAutoRefresh();
        else supabase.auth.stopAutoRefresh();
      });
    }
  3. Read the token when you need it. Queries through supabase.from(...) already send the current token. For your own fetch calls, ask for the session right before the request, because getSession() refreshes it if needed:

    const { data } = await supabase.auth.getSession();
    const token = data.session?.access_token;
  4. Handle real sign-outs. Listen with supabase.auth.onAuthStateChange and send people to your sign-in screen on SIGNED_OUT, which is what you’ll get when a session ends because of a sign-out, a password change, or a session time limit you’ve set.

  5. Keep the 1 hour default. We’d leave it alone, because a longer expiry only delays the error, and Supabase’s docs advise against going below 5 minutes.

If you lock the app with Face ID or a fingerprint, keep the Supabase session stored as above and treat the unlock as a gate in front of it. After someone unlocks, call getSession(), and the stored refresh token is used to get a fresh access token, so the app never needs their password.

How you know it’s fixed

To test it, you can leave the app open for more than an hour and put it in the background for a while before you bring it back and load data. Requests keep working, and you’ll see TOKEN_REFRESHED events in onAuthStateChange instead of errors.

If it still happens

Search your code for access_token to find any saved copy, and check that only one Supabase client exists. Refresh timing uses the phone’s clock, so a phone set far from the real time can send tokens that look valid to the app but expired to the server. JWT issued at future is the same check from the other side: the server allows 30 seconds of difference, and the token’s issue time is further ahead than that. Tokens are issued on Supabase’s side, so if that one keeps happening, contact Supabase support with your project and the times it occurred.

Ask the agent

If your app is in Draftbit, paste this into the agent, and it can read your code and make the change for you.

My app gets JWT expired errors from Supabase after it's been open for a while. Set up the Supabase client so the session is stored and refreshed automatically, stop any code that saves the access token itself, and send people to sign in again only when their session has really ended.

Open Draftbit

More Supabase answers