The Supabase JS client automatically refreshes session tokens before they expire using the refresh token stored in localStorage or cookies. You do not need to manually refresh in most cases. If you need to force a refresh, call supabase.auth.refreshSession(). Listen for the TOKEN_REFRESHED event via onAuthStateChange to react when a new token is issued. The default JWT expiry is 3600 seconds (1 hour), configurable in the Dashboard.
Understanding and Managing Session Token Refresh in Supabase
Supabase Auth uses JWTs (JSON Web Tokens) for session management. Each JWT has an expiry time, and the client automatically refreshes the token before it expires using a long-lived refresh token. This tutorial explains how the auto-refresh mechanism works, when you might need to manually refresh, and how to handle edge cases like background tabs, network interruptions, and long-running sessions.
Prerequisites
- A Supabase project with authentication enabled
- The Supabase JS client installed (@supabase/supabase-js v2)
- A basic understanding of how JWTs and refresh tokens work
Step-by-step guide
Understand how automatic token refresh works
Understand how automatic token refresh works
When a user logs in, Supabase returns two tokens: an access token (JWT) and a refresh token. The access token has a short lifespan (default 3600 seconds / 1 hour) and is sent with every API request. The refresh token has a much longer lifespan and is used to obtain new access tokens. The Supabase JS client automatically refreshes the access token when approximately 80% of its lifetime has elapsed. This happens silently in the background — no user interaction or code is needed.
1// The client handles refresh automatically — this just works2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!7)89// autoRefreshToken is true by default10// The client will refresh the token ~12 minutes before expiry (for 1-hour tokens)Expected result: The client silently refreshes the access token before it expires, and API requests continue to work without interruption.
Listen for TOKEN_REFRESHED events
Listen for TOKEN_REFRESHED events
The onAuthStateChange listener fires a TOKEN_REFRESHED event every time the client successfully refreshes the access token. This is useful for updating your app state with the new session data, logging token refreshes for debugging, or syncing the session across components. Register the listener in your root component and clean it up on unmount to prevent memory leaks.
1import { supabase } from '@/lib/supabase'23const { data: { subscription } } = supabase.auth.onAuthStateChange(4 (event, session) => {5 switch (event) {6 case 'TOKEN_REFRESHED':7 console.log('Token was refreshed at:', new Date().toISOString())8 console.log('New expiry:', new Date(session!.expires_at! * 1000).toISOString())9 break10 case 'SIGNED_OUT':11 console.log('User signed out')12 break13 case 'SIGNED_IN':14 console.log('User signed in')15 break16 }17 }18)1920// Clean up when done21// subscription.unsubscribe()Expected result: Your callback fires every time the token is refreshed, giving you the updated session with the new access token and expiry time.
Manually refresh the session when needed
Manually refresh the session when needed
In most cases, automatic refresh is sufficient. However, there are edge cases where you may need to force a refresh: after a user updates their profile metadata (which is embedded in the JWT), when recovering from a network outage, or when your app detects an expired token. Call supabase.auth.refreshSession() to immediately obtain a new access token using the stored refresh token.
1// Force a manual refresh2const { data: { session }, error } = await supabase.auth.refreshSession()34if (error) {5 console.error('Refresh failed:', error.message)6 // If refresh fails, the refresh token is likely expired7 // Redirect the user to login8 window.location.href = '/login'9} else {10 console.log('New session:', session)11 console.log('Token expires at:', new Date(session!.expires_at! * 1000))12}1314// You can also pass an existing refresh token explicitly15const { data, error: err } = await supabase.auth.refreshSession({16 refresh_token: 'your-refresh-token-here'17})Expected result: A new access token is issued immediately, and the session is updated in localStorage or cookies.
Configure JWT expiry time in the Dashboard
Configure JWT expiry time in the Dashboard
The default JWT expiry is 3600 seconds (1 hour). You can change this in the Supabase Dashboard. Go to Authentication > Settings > JWT Expiry and enter a new value in seconds. Shorter expiry times are more secure (less window if a token is stolen) but increase the frequency of refresh requests. Longer expiry times reduce API calls but increase the security risk window. Most production apps keep the default of 3600 seconds.
Expected result: New tokens issued after the change will use the updated expiry time. Existing sessions will continue with their original expiry until they are refreshed.
Handle token refresh failures gracefully
Handle token refresh failures gracefully
Token refresh can fail for several reasons: the refresh token has expired (default 1 week), the user has been deleted, the network is down, or the Supabase project is paused. Your app should handle these failures gracefully by detecting the error and redirecting the user to login. Listen for the SIGNED_OUT event which fires when the client cannot refresh the session and gives up.
1import { supabase } from '@/lib/supabase'23supabase.auth.onAuthStateChange((event, session) => {4 if (event === 'SIGNED_OUT') {5 // Session expired and could not be refreshed6 // Clear any local state7 localStorage.removeItem('user-preferences')8 // Redirect to login9 window.location.href = '/login'10 }11})1213// Wrap API calls to handle expired tokens14async function fetchWithAuth(tableName: string) {15 const { data, error } = await supabase16 .from(tableName)17 .select('*')1819 if (error?.message?.includes('JWT expired')) {20 // Try to refresh21 const { error: refreshError } = await supabase.auth.refreshSession()22 if (refreshError) {23 window.location.href = '/login'24 return null25 }26 // Retry the original request27 const { data: retryData } = await supabase.from(tableName).select('*')28 return retryData29 }3031 return data32}Expected result: Your app detects refresh failures and gracefully redirects to the login page instead of showing broken API error states.
Complete working example
1// Complete session refresh management for Supabase2import { createClient, Session } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!7)89// Track the current session10let currentSession: Session | null = null1112// Register the auth state listener13const { data: { subscription } } = supabase.auth.onAuthStateChange(14 (event, session) => {15 currentSession = session1617 switch (event) {18 case 'INITIAL_SESSION':19 console.log('Session loaded from storage')20 break21 case 'SIGNED_IN':22 console.log('User signed in:', session?.user?.email)23 break24 case 'TOKEN_REFRESHED':25 console.log('Token refreshed, new expiry:',26 session?.expires_at27 ? new Date(session.expires_at * 1000).toISOString()28 : 'unknown'29 )30 break31 case 'SIGNED_OUT':32 console.log('User signed out or session expired')33 handleSignOut()34 break35 }36 }37)3839// Force refresh (call after profile updates)40async function forceRefresh(): Promise<Session | null> {41 const { data: { session }, error } = await supabase.auth.refreshSession()42 if (error) {43 console.error('Refresh failed:', error.message)44 return null45 }46 return session47}4849// Check if the current token is close to expiring50function isTokenExpiringSoon(): boolean {51 if (!currentSession?.expires_at) return true52 const expiresAt = currentSession.expires_at * 100053 const now = Date.now()54 const fiveMinutes = 5 * 60 * 100055 return (expiresAt - now) < fiveMinutes56}5758// Handle sign out / expired session59function handleSignOut() {60 currentSession = null61 if (typeof window !== 'undefined') {62 window.location.href = '/login'63 }64}6566// Cleanup67function cleanup() {68 subscription.unsubscribe()69}7071export { supabase, forceRefresh, isTokenExpiringSoon, cleanup }Common mistakes when refreshing a Session Token in Supabase
Why it's a problem: Manually implementing a setInterval to refresh tokens instead of relying on the built-in auto-refresh
How to avoid: The Supabase client handles token refresh automatically. Remove any custom refresh intervals. They can cause race conditions and duplicate refresh requests.
Why it's a problem: Setting autoRefreshToken to false without implementing a custom refresh mechanism
How to avoid: Leave autoRefreshToken at its default value of true. Only set it to false if you have a specific architectural reason and are handling refresh yourself.
Why it's a problem: Not handling the case where refreshSession() fails because the refresh token has expired
How to avoid: Always check for errors from refreshSession(). If it fails, redirect the user to the login page. The refresh token has a separate (longer) expiry from the access token.
Best practices
- Leave autoRefreshToken enabled (the default) and let the Supabase client handle token refresh automatically
- Register onAuthStateChange in your root component to react to TOKEN_REFRESHED and SIGNED_OUT events
- Call refreshSession() manually only when needed, such as after updating user metadata or recovering from errors
- Keep the default JWT expiry of 3600 seconds unless you have a specific security requirement to change it
- Handle the SIGNED_OUT event to gracefully redirect users to the login page when sessions expire
- Never store the refresh token in a custom location — let the Supabase client manage token storage
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Explain how Supabase session token refresh works, including auto-refresh, manual refresh with refreshSession(), and the TOKEN_REFRESHED event. Show TypeScript code for handling all token lifecycle events in a React app.
Show me how to manage Supabase session token refresh in a React app. Include onAuthStateChange setup for TOKEN_REFRESHED events, manual refresh after profile updates, and graceful handling when the refresh token expires.
Frequently asked questions
Does the Supabase client refresh tokens automatically?
Yes. The client refreshes the access token when approximately 80% of its lifetime has elapsed. With the default 1-hour expiry, the client refreshes around the 48-minute mark. No custom code is needed.
When should I call refreshSession() manually?
Call it after updating user metadata with updateUser(), when recovering from a network outage, or when your app detects a JWT expired error. In normal operation, the auto-refresh handles everything.
What happens if the refresh token itself expires?
If the refresh token expires, the session cannot be renewed and the user must log in again. The SIGNED_OUT event fires automatically. The default refresh token lifetime is configurable in the Dashboard.
Does token refresh work in background tabs?
Browsers throttle timers in background tabs, which can delay the refresh. The Supabase client uses a visibility change listener to attempt a refresh when the tab becomes active again. Very long periods in background (hours) may cause the token to expire.
Can I change the JWT expiry time?
Yes. Go to Dashboard > Authentication > Settings and update the JWT Expiry field. The value is in seconds. Changes only affect new tokens — existing sessions keep their original expiry until they are refreshed.
How do I know when a token was last refreshed?
Listen for the TOKEN_REFRESHED event via onAuthStateChange. The session object in the callback contains expires_at, which tells you when the new token will expire. Log this value during development for debugging.
Can RapidDev help me configure session management for my Supabase app?
Yes. RapidDev can help you configure JWT settings, implement custom refresh logic, set up SSR-compatible sessions, and handle complex auth flows across multiple platforms.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation