Skip to content

Sessions and JWTs

A successful sign-in gets you a short-lived JWT access token plus a longer-lived refresh token, supabase-js stores and silently refreshes both for you, and it is the claims inside that JWT — the user’s ID and role — that Postgres uses to decide what a request is allowed to see.

Every sign-in returns a session containing an access_token (a JWT, short-lived, typically an hour) and a refresh_token (long-lived, used to silently mint a new access token when the old one expires). supabase-js handles the storage and the refresh cycle for you — you do not need to manually track expiry or call a refresh endpoint yourself.

const {
data: { session },
} = await supabase.auth.getSession();
console.log(session?.access_token); // short-lived JWT
console.log(session?.refresh_token); // used to silently mint a new access token later

The JWT itself carries claims — a sub claim holding the user’s ID, plus a role claim (typically authenticated for a signed-in user, anon for an unauthenticated request). Every request supabase-js makes to the auto-generated API attaches this JWT, and Postgres and PostgREST read those claims on the way in. That is exactly what powers auth.uid() inside a row level security policy, which the next lesson covers in depth — the JWT sent with a request is what lets a policy check whether a row belongs to the current user.

Reading the current session: getSession vs getUser

Section titled “Reading the current session: getSession vs getUser”

supabase.auth.getSession() reads the session directly from local storage (refreshing it if the access token has expired), so it returns instantly without a round trip to the Auth server. supabase.auth.getUser() instead sends the access token to the Auth server to be revalidated, so it is slower but authoritative — reach for getUser() whenever you need a guarantee that the token has not been tampered with or revoked, such as before performing a sensitive server-side action.

// Fast: reads the locally stored session, refreshing it if needed
const { data: { session } } = await supabase.auth.getSession();
// Authoritative: revalidates the access token against the Auth server
const { data: { user }, error } = await supabase.auth.getUser();

Sign-in, sign-out, and token refresh all happen asynchronously, so UI code needs a way to react to them rather than polling. supabase.auth.onAuthStateChange registers a listener that fires on every one of those events with the event name and the current session, which is the standard way to keep client-side UI state (a signed-in nav bar, a redirect after logout, and so on) in sync with the actual auth state.

const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session?.user?.id);
// event is one of: SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED, ...
});
// Later, when the listener is no longer needed:
authListener.subscription.unsubscribe();
flowchart LR
  signin["User signs in"] --> issue["Auth issues access token (JWT) + refresh token"]
  issue --> attach["supabase-js attaches JWT to every API request"]
  attach --> pg[("Postgres / PostgREST reads JWT claims")]
  pg --> uid["auth.uid() available inside RLS policies"]
From sign-in to a JWT Postgres can evaluate
What does a JWT issued by Supabase Auth make available inside a row level security policy?
What is onAuthStateChange used for?
What is the key difference between getSession() and getUser()?
What two tokens does supabase-js manage on your behalf after sign-in?