Sessions and JWTs
The idea in one sentence
Section titled “The idea in one sentence”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.
What you get back after signing in
Section titled “What you get back after signing in”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 JWTconsole.log(session?.refresh_token); // used to silently mint a new access token laterThe 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 neededconst { data: { session } } = await supabase.auth.getSession();
// Authoritative: revalidates the access token against the Auth serverconst { data: { user }, error } = await supabase.auth.getUser();Staying in sync: onAuthStateChange
Section titled “Staying in sync: onAuthStateChange”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"]