Sessions, JWTs and refresh tokens

How Supabase Auth sessions work on SupaHost: access token lifetime (300 to 604800 seconds), the claims inside the JWT, refresh tokens, signing out, and verifying tokens on your own server.

A Supabase session is a pair of tokens:

  • An access token: a short-lived JWT sent with every request. The Data API, Storage, Realtime and Edge Functions read the user's identity and role from it, and your RLS policies read its claims.
  • A refresh token: a long-lived, single-use string the client exchanges for a new access token (and a new refresh token) before the old access token expires.

The client libraries handle the refresh for you. In the browser, supabase-js refreshes in the background while the tab is open. On servers, @supabase/ssr refreshes when a request arrives with an expired or nearly expired token (see Server-side auth).

Access token lifetime

Set it in Dashboard → your project → Auth tab → Sign-in & security → Access token lifetime, in seconds:

Value
Default 3600 (one hour)
Minimum 300 (five minutes)
Maximum 604800 (seven days)

Click Save sign-in settings; the new value applies to tokens issued from about a minute later. Tokens already issued keep their original expiry.

Shorter lifetimes mean changes such as a removed role or a banned user take effect sooner, because the old token stops working sooner. They also mean more refresh requests. One hour suits most apps. Going much lower rarely helps, and going much higher means a leaked token stays useful for longer.

The access token lifetime isn't the session length. A user stays signed in as long as their client keeps refreshing, which can be weeks or months. SupaHost has no setting for a maximum session length or an inactivity timeout, so sessions last until the user signs out, you sign them out, or the user is deleted.

What's in the access token

Decoded, a SupaHost access token looks like this:

{
  "iss": "https://app-one.acme.supahost.space/auth/v1",
  "sub": "8c3e6f1a-2b4d-4c9e-9f10-5a7b3c2d1e0f",
  "aud": "authenticated",
  "exp": 1794567600,
  "iat": 1794564000,
  "role": "authenticated",
  "email": "[email protected]",
  "aal": "aal1",
  "amr": [{ "method": "password", "timestamp": 1794564000 }],
  "session_id": "1f2e3d4c-5b6a-4789-8a9b-0c1d2e3f4a5b",
  "is_anonymous": false,
  "app_metadata": { "provider": "email", "providers": ["email"] },
  "user_metadata": { "display_name": "Dana" }
}
  • sub is the user id, which auth.uid() returns in SQL.
  • role decides the Postgres role for the request (authenticated for signed-in users).
  • aal is aal2 once the user has passed an MFA check.
  • iss always uses the project's SupaHost host, even after you add a custom domain.

Read claims in policies with auth.jwt():

create policy "Only confirmed MFA sessions see payouts"
on public.payouts for select
to authenticated
using ((select auth.jwt() ->> 'aal') = 'aal2' and (select auth.uid()) = user_id);

The anon and service_role API keys are JWTs too, with iss: "supahost" and role set to anon or service_role. They aren't sessions and don't expire for ten years. See API keys.

Refresh tokens

Each refresh token can be used once. Using it returns a new access token and a new refresh token, and the old one is retired. Auth allows a short grace period (GoTrue's default is 10 seconds) in which the same refresh token can be presented again, to cover two tabs refreshing at once or a response lost on a flaky network.

If an old refresh token turns up after that window, Auth treats it as possible theft and revokes the whole session. In practice this happens most often when two parts of the same app refresh independently, for example a server middleware and a browser client using different cookie or storage setups. If users are signed out "at random", look for that first.

Signing out

await supabase.auth.signOut()                     // every session of this user (default scope 'global')
await supabase.auth.signOut({ scope: 'local' })   // only the session on this device
await supabase.auth.signOut({ scope: 'others' })  // every session except this one

Signing out revokes the refresh tokens, so the sessions can't be renewed. Access tokens already issued stay valid until they expire, because they're checked by signature, not looked up. Keep the access token lifetime modest if you need sign-out to bite quickly.

To sign a user out of everything from your server (after support resets their password, say): if you hold one of their access tokens, admin.auth.admin.signOut(accessToken, 'global') revokes all their sessions. Without a token, deleting the user's rows from auth.sessions in the SQL editor has the same effect, because their refresh tokens are removed with them. See Managing users.

Reacting to session changes

const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
  // INITIAL_SESSION, SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED, PASSWORD_RECOVERY
  if (event === 'SIGNED_OUT') clearLocalData()
})

// later
subscription.unsubscribe()

Keep the callback quick and don't await other Supabase calls inside it; schedule them with setTimeout instead, or you can deadlock the client.

Verifying tokens on your own server

If a separate backend receives a user's access token (in an Authorization: Bearer header from your app), verify it before trusting it. The simplest way works with any SupaHost project:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

export async function userFromRequest(req: Request) {
  const token = req.headers.get('authorization')?.replace(/^Bearer /, '')
  if (!token) return null
  const { data, error } = await supabase.auth.getUser(token)
  return error ? null : data.user
}

SupaHost projects sign tokens with HS256, a shared secret. The JWT secret isn't shown in the dashboard and can't be rotated, so verify through Auth (getUser(token) or getClaims(token)) rather than with a JWT library. In Edge Functions, the "Require JWT" option rejects requests without a valid project JWT, but the anon key counts as one, so still look up the user inside the function. See Authentication in Edge Functions.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us