Supabase JWTs expire after 3600 seconds (1 hour) by default. The Supabase client automatically refreshes tokens before they expire using the refresh token. To handle edge cases, listen for the TOKEN_REFRESHED event via onAuthStateChange, configure the JWT expiry time in the Dashboard under Settings > Auth, and always use getUser() instead of getSession() for server-side verification since getSession() reads from local cache without verifying expiration.
Handling JWT Expiration in Supabase Auth
Every authenticated Supabase request includes a JWT (JSON Web Token) that contains the user's identity and role. These tokens expire after a configurable period, and the Supabase client handles renewal automatically in most cases. But edge cases exist — background tabs, long-running operations, and server-side verification all require special attention. This tutorial explains how JWT expiration works in Supabase, how to configure it, and how to handle failures gracefully.
Prerequisites
- A Supabase project with Auth enabled
- Basic understanding of JWTs and authentication tokens
- @supabase/supabase-js v2+ installed in your project
- An existing login flow in your application
Step-by-step guide
Understand how Supabase JWT auto-refresh works
Understand how Supabase JWT auto-refresh works
When a user signs in, Supabase issues two tokens: an access token (JWT) and a refresh token. The access token is short-lived (default 3600 seconds) and is sent with every API request. The refresh token is long-lived and is used to get a new access token when the current one expires. The Supabase client automatically refreshes the access token before it expires, typically 60 seconds before expiry. This happens transparently — you do not need to write any code for the standard case.
1// The Supabase client handles auto-refresh internally.2// When you create the client, auto-refresh is enabled by default:3import { createClient } from '@supabase/supabase-js'45const supabase = createClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,8 {9 auth: {10 autoRefreshToken: true, // default: true11 persistSession: true, // default: true12 },13 }14)Expected result: You understand that the Supabase client refreshes tokens automatically before they expire.
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 adjust this in the Supabase Dashboard. Go to Settings > Auth > Auth Settings and find the JWT Expiry field. Shorter expiry times (e.g., 900 seconds / 15 minutes) are more secure but require more frequent token refreshes. Longer times (e.g., 86400 seconds / 24 hours) reduce refresh requests but increase the window of vulnerability if a token is leaked. For most applications, the default of 3600 seconds is a good balance.
1# JWT expiry is configured in the Dashboard:2# Settings > Auth > Auth Settings > JWT Expiry3#4# Common values:5# 900 = 15 minutes (high security)6# 3600 = 1 hour (default, recommended)7# 86400 = 24 hours (convenience, lower security)8#9# You can also set it via environment variable:10# GOTRUE_JWT_EXP=3600Expected result: The JWT expiry time is configured in the Dashboard to match your security requirements.
Listen for token refresh events with onAuthStateChange
Listen for token refresh events with onAuthStateChange
The onAuthStateChange listener fires whenever the auth state changes, including when tokens are refreshed. Listen for the TOKEN_REFRESHED event to confirm that auto-refresh is working and to handle edge cases. If a refresh fails (for example, because the refresh token expired or the user revoked their session), you will receive a SIGNED_OUT event instead. Use this to redirect the user to the login page or show a re-authentication prompt.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78const { data: { subscription } } = supabase.auth.onAuthStateChange(9 (event, session) => {10 switch (event) {11 case 'TOKEN_REFRESHED':12 console.log('Token refreshed successfully')13 // Optionally update any stored session data14 break15 case 'SIGNED_OUT':16 console.log('User signed out or token refresh failed')17 // Redirect to login page18 window.location.href = '/login'19 break20 case 'SIGNED_IN':21 console.log('User signed in', session?.user.email)22 break23 }24 }25)2627// Cleanup when component unmounts28// subscription.unsubscribe()Expected result: Your application listens for TOKEN_REFRESHED and SIGNED_OUT events and responds appropriately.
Handle expired tokens in API requests
Handle expired tokens in API requests
In rare cases, a token can expire between the auto-refresh check and your API request. This results in a 401 error from the Supabase API. Handle this by catching the error, manually refreshing the session, and retrying the request. This is especially important for long-running operations where the token might expire mid-process. The Supabase client's built-in retry logic handles most cases, but explicit handling adds robustness.
1async function fetchDataWithRetry(tableName: string) {2 const { data, error } = await supabase3 .from(tableName)4 .select('*')56 if (error && error.message.includes('JWT expired')) {7 // Token expired between refresh and request8 const { error: refreshError } = await supabase.auth.refreshSession()910 if (refreshError) {11 // Refresh token also expired — user must re-authenticate12 console.error('Session expired. Please log in again.')13 window.location.href = '/login'14 return null15 }1617 // Retry the original request with the new token18 const { data: retryData, error: retryError } = await supabase19 .from(tableName)20 .select('*')2122 if (retryError) throw retryError23 return retryData24 }2526 if (error) throw error27 return data28}Expected result: Your application gracefully handles JWT expiration by refreshing the token and retrying failed requests.
Use getUser() instead of getSession() for server-side verification
Use getUser() instead of getSession() for server-side verification
On the server side, never trust getSession() to verify whether a token is valid. getSession() reads the JWT from cookies or local storage without making an API call — it does not check whether the token has expired or been revoked. Always use getUser(), which sends the token to the Supabase Auth server for verification. If the token is expired and cannot be refreshed, getUser() returns an error instead of stale user data.
1// WRONG: getSession() does not verify the token2// It may return an expired session that looks valid3const { data: { session } } = await supabase.auth.getSession()4// session.user may exist even if the JWT is expired!56// CORRECT: getUser() verifies the token with the server7const { data: { user }, error } = await supabase.auth.getUser()89if (error || !user) {10 // Token is expired or invalid — redirect to login11 redirect('/login')12}1314// Safe to proceed with authenticated operations15const { data } = await supabase16 .from('profiles')17 .select('*')18 .eq('id', user.id)19 .single()Expected result: Server-side code uses getUser() for reliable token verification that catches expired JWTs.
Complete working example
1// Complete JWT expiration handling for Supabase2import { createClient, AuthChangeEvent, Session } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,7 {8 auth: {9 autoRefreshToken: true,10 persistSession: true,11 },12 }13)1415// Listen for auth state changes including token refresh16function setupAuthListener() {17 const { data: { subscription } } = supabase.auth.onAuthStateChange(18 (event: AuthChangeEvent, session: Session | null) => {19 switch (event) {20 case 'TOKEN_REFRESHED':21 console.log('JWT auto-refreshed at', new Date().toISOString())22 break23 case 'SIGNED_OUT':24 console.log('Session ended — redirecting to login')25 window.location.href = '/login'26 break27 case 'SIGNED_IN':28 console.log('User signed in:', session?.user?.email)29 break30 }31 }32 )33 return subscription34}3536// Fetch data with automatic retry on JWT expiration37async function fetchWithAuth<T>(38 queryFn: () => Promise<{ data: T | null; error: any }>39): Promise<T | null> {40 const { data, error } = await queryFn()4142 if (error?.message?.includes('JWT expired')) {43 const { error: refreshError } = await supabase.auth.refreshSession()44 if (refreshError) {45 window.location.href = '/login'46 return null47 }48 const { data: retryData, error: retryError } = await queryFn()49 if (retryError) throw retryError50 return retryData51 }5253 if (error) throw error54 return data55}5657// Server-side: Always use getUser(), never getSession()58async function getAuthenticatedUser() {59 const { data: { user }, error } = await supabase.auth.getUser()60 if (error || !user) return null61 return user62}6364export { supabase, setupAuthListener, fetchWithAuth, getAuthenticatedUser }Common mistakes when handling JWT Expiration in Supabase
Why it's a problem: Using getSession() on the server to check if a user is authenticated, which returns stale data without verifying the JWT
How to avoid: Always use getUser() for server-side auth checks. It sends the JWT to the Supabase Auth server for verification and catches expired tokens.
Why it's a problem: Setting JWT expiry too short (under 300 seconds), causing excessive token refresh requests
How to avoid: Keep JWT expiry at 3600 seconds (1 hour) or above for most applications. The Supabase client needs time between refresh cycles.
Why it's a problem: Disabling autoRefreshToken in client-side code without implementing manual refresh logic
How to avoid: Leave autoRefreshToken set to true (the default). Only disable it in server-side admin clients where session persistence is not needed.
Why it's a problem: Not handling the SIGNED_OUT event when token refresh fails, leaving the user in a broken state
How to avoid: Listen for SIGNED_OUT in onAuthStateChange and redirect the user to the login page. This covers cases where the refresh token itself has expired.
Best practices
- Keep the default JWT expiry of 3600 seconds unless you have specific security requirements
- Always use getUser() instead of getSession() for server-side authentication checks
- Listen for TOKEN_REFRESHED and SIGNED_OUT events with onAuthStateChange
- Implement retry logic for API requests that fail with JWT expired errors
- Clean up onAuthStateChange subscriptions when components unmount to prevent memory leaks
- Never set autoRefreshToken to false in client-side code
- Use @supabase/ssr for SSR frameworks to get cookie-based session management that handles token refresh correctly
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Explain how JWT expiration works in Supabase Auth. Show me how to configure the expiry time, listen for token refresh events with onAuthStateChange, handle expired tokens in API requests with retry logic, and correctly use getUser() vs getSession() for server-side verification.
Create a Supabase auth utility module that sets up an onAuthStateChange listener for TOKEN_REFRESHED and SIGNED_OUT events, implements a fetchWithAuth wrapper that retries requests on JWT expiration, and exports a getAuthenticatedUser function that uses getUser() for server-side verification.
Frequently asked questions
What is the default JWT expiry time in Supabase?
The default JWT expiry is 3600 seconds (1 hour). You can change this in the Supabase Dashboard under Settings > Auth > Auth Settings > JWT Expiry.
Does the Supabase client automatically refresh expired tokens?
Yes. The client refreshes the access token automatically about 60 seconds before it expires, using the refresh token. This happens transparently as long as autoRefreshToken is true (the default).
What happens if the refresh token expires?
If the refresh token expires, the auto-refresh fails and the user is effectively signed out. Your onAuthStateChange listener will receive a SIGNED_OUT event. The user must sign in again to get new tokens.
Should I use getSession() or getUser() to check if a token is valid?
Use getUser() for any security-sensitive check, especially on the server. getSession() reads from local storage without verifying the token and may return an expired session that appears valid. getUser() sends the token to the Supabase Auth server for verification.
Can I set different JWT expiry times for different users?
No. JWT expiry is a project-wide setting. All users in the same Supabase project share the same expiry time. If you need different session durations, implement custom logic in your application.
Why does my user get logged out when switching browser tabs?
Browser throttling can pause JavaScript timers in background tabs, preventing the auto-refresh from running. When the tab becomes active again, the token may already be expired. The client will attempt a refresh, but if it fails, the user is signed out. Listening for visibilitychange events and calling refreshSession() when the tab becomes visible can help.
Can RapidDev help implement robust session management with Supabase?
Yes. RapidDev can configure JWT expiry settings, implement token refresh retry logic, set up proper onAuthStateChange listeners, and ensure your server-side auth checks use getUser() correctly across your entire application.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation