Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Handle JWT Expiration in Supabase

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.

What you'll learn

  • How Supabase JWT auto-refresh works and when it can fail
  • How to configure JWT expiry time in the Supabase Dashboard
  • How to handle token expiration gracefully in your application
  • The critical difference between getSession() and getUser() for token validation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read15-20 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
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'
4
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
8 {
9 auth: {
10 autoRefreshToken: true, // default: true
11 persistSession: true, // default: true
12 },
13 }
14)

Expected result: You understand that the Supabase client refreshes tokens automatically before they expire.

2

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.

typescript
1# JWT expiry is configured in the Dashboard:
2# Settings > Auth > Auth Settings > JWT Expiry
3#
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=3600

Expected result: The JWT expiry time is configured in the Dashboard to match your security requirements.

3

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.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8const { 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 data
14 break
15 case 'SIGNED_OUT':
16 console.log('User signed out or token refresh failed')
17 // Redirect to login page
18 window.location.href = '/login'
19 break
20 case 'SIGNED_IN':
21 console.log('User signed in', session?.user.email)
22 break
23 }
24 }
25)
26
27// Cleanup when component unmounts
28// subscription.unsubscribe()

Expected result: Your application listens for TOKEN_REFRESHED and SIGNED_OUT events and responds appropriately.

4

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.

typescript
1async function fetchDataWithRetry(tableName: string) {
2 const { data, error } = await supabase
3 .from(tableName)
4 .select('*')
5
6 if (error && error.message.includes('JWT expired')) {
7 // Token expired between refresh and request
8 const { error: refreshError } = await supabase.auth.refreshSession()
9
10 if (refreshError) {
11 // Refresh token also expired — user must re-authenticate
12 console.error('Session expired. Please log in again.')
13 window.location.href = '/login'
14 return null
15 }
16
17 // Retry the original request with the new token
18 const { data: retryData, error: retryError } = await supabase
19 .from(tableName)
20 .select('*')
21
22 if (retryError) throw retryError
23 return retryData
24 }
25
26 if (error) throw error
27 return data
28}

Expected result: Your application gracefully handles JWT expiration by refreshing the token and retrying failed requests.

5

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.

typescript
1// WRONG: getSession() does not verify the token
2// It may return an expired session that looks valid
3const { data: { session } } = await supabase.auth.getSession()
4// session.user may exist even if the JWT is expired!
5
6// CORRECT: getUser() verifies the token with the server
7const { data: { user }, error } = await supabase.auth.getUser()
8
9if (error || !user) {
10 // Token is expired or invalid — redirect to login
11 redirect('/login')
12}
13
14// Safe to proceed with authenticated operations
15const { data } = await supabase
16 .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

supabase-auth-session.ts
1// Complete JWT expiration handling for Supabase
2import { createClient, AuthChangeEvent, Session } from '@supabase/supabase-js'
3
4const 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)
14
15// Listen for auth state changes including token refresh
16function 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 break
23 case 'SIGNED_OUT':
24 console.log('Session ended — redirecting to login')
25 window.location.href = '/login'
26 break
27 case 'SIGNED_IN':
28 console.log('User signed in:', session?.user?.email)
29 break
30 }
31 }
32 )
33 return subscription
34}
35
36// Fetch data with automatic retry on JWT expiration
37async function fetchWithAuth<T>(
38 queryFn: () => Promise<{ data: T | null; error: any }>
39): Promise<T | null> {
40 const { data, error } = await queryFn()
41
42 if (error?.message?.includes('JWT expired')) {
43 const { error: refreshError } = await supabase.auth.refreshSession()
44 if (refreshError) {
45 window.location.href = '/login'
46 return null
47 }
48 const { data: retryData, error: retryError } = await queryFn()
49 if (retryError) throw retryError
50 return retryData
51 }
52
53 if (error) throw error
54 return data
55}
56
57// Server-side: Always use getUser(), never getSession()
58async function getAuthenticatedUser() {
59 const { data: { user }, error } = await supabase.auth.getUser()
60 if (error || !user) return null
61 return user
62}
63
64export { 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.