When Supabase auth sessions disappear on page reload, the issue is usually caused by creating multiple Supabase client instances, missing the onAuthStateChange listener, incorrect cookie configuration in SSR frameworks, or localStorage being cleared. Fix it by ensuring a single shared client instance, registering the auth state listener on app startup, and using @supabase/ssr for server-rendered apps like Next.js.
Diagnosing and Fixing Session Persistence Issues in Supabase Auth
One of the most common problems Supabase developers face is sessions that vanish after a page reload or when navigating between pages. The Supabase JS client automatically stores session tokens in localStorage (client-side) or cookies (SSR), but several configuration mistakes can break this behavior. This tutorial walks through every common cause — from multiple client instances to SSR cookie misconfiguration — and shows you how to fix each one.
Prerequisites
- A Supabase project with authentication enabled
- A frontend app using @supabase/supabase-js v2
- Basic understanding of how browser localStorage and cookies work
- For SSR: familiarity with Next.js App Router or similar framework
Step-by-step guide
Verify you have a single shared Supabase client instance
Verify you have a single shared Supabase client instance
The most common cause of session loss is creating a new Supabase client on every render or in every component. Each call to createClient() creates an independent client with its own session state. If you create the client inside a component, it will not share the session with other components, and the session will appear to vanish. Instead, create the client once in a shared module and import it everywhere. In React, this means a dedicated file like lib/supabase.ts that exports a single client instance.
1// lib/supabase.ts — create this file ONCE and import everywhere2import { createClient } from '@supabase/supabase-js'34const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!5const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!67export const supabase = createClient(supabaseUrl, supabaseAnonKey)89// In any component:10import { supabase } from '@/lib/supabase'Expected result: All components share the same Supabase client instance and can access the same session.
Register onAuthStateChange on app startup
Register onAuthStateChange on app startup
The Supabase client uses onAuthStateChange to synchronize the session across tabs and to handle token refresh. If you never register this listener, the client cannot respond to TOKEN_REFRESHED events, and expired sessions will not auto-renew. Register the listener in your app's root component (e.g., _app.tsx, layout.tsx, or App.tsx) and clean it up on unmount. The listener fires with INITIAL_SESSION on first load, giving you the current session state.
1import { useEffect, useState } from 'react'2import { supabase } from '@/lib/supabase'3import type { Session } from '@supabase/supabase-js'45export function AuthProvider({ children }: { children: React.ReactNode }) {6 const [session, setSession] = useState<Session | null>(null)78 useEffect(() => {9 const { data: { subscription } } = supabase.auth.onAuthStateChange(10 (event, session) => {11 console.log('Auth event:', event)12 setSession(session)13 }14 )1516 return () => subscription.unsubscribe()17 }, [])1819 return <>{children}</>20}Expected result: The auth state listener fires on every auth event, and your app state stays in sync with the current session.
Check localStorage for session tokens (client-side apps)
Check localStorage for session tokens (client-side apps)
For client-side rendered apps (React SPA, Vite, Create React App), Supabase stores session tokens in localStorage under a key like sb-<project-ref>-auth-token. Open your browser DevTools, go to Application > Local Storage, and check for this key. If it is missing after login, something is clearing localStorage or the client is misconfigured. Common causes include: calling supabase.auth.signOut() unintentionally, browser extensions that clear storage, or using incognito mode with aggressive cookie/storage policies.
1// Debug helper: check what is in localStorage2const storageKey = `sb-${supabaseUrl.split('//')[1].split('.')[0]}-auth-token`3const storedSession = localStorage.getItem(storageKey)4console.log('Stored session:', storedSession ? 'EXISTS' : 'MISSING')56// If you need to manually check the current session:7const { data: { session } } = await supabase.auth.getSession()8console.log('Current session:', session)Expected result: You can see the session token in localStorage and confirm whether the client is persisting it correctly.
Configure cookie-based sessions for SSR with @supabase/ssr
Configure cookie-based sessions for SSR with @supabase/ssr
If you are using Next.js (App Router), SvelteKit, or any server-rendered framework, localStorage is not available on the server. You need @supabase/ssr, which stores the session in cookies instead. Install the package and create separate client factories for browser and server contexts. The browser client reads cookies and keeps them updated. The server client reads cookies from the request headers. You also need Next.js middleware to refresh expired sessions on every request.
1// lib/supabase/client.ts — for Client Components2import { createBrowserClient } from '@supabase/ssr'34export function createClient() {5 return createBrowserClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!8 )9}1011// lib/supabase/server.ts — for Server Components & Route Handlers12import { createServerClient } from '@supabase/ssr'13import { cookies } from 'next/headers'1415export async function createClient() {16 const cookieStore = await cookies()17 return createServerClient(18 process.env.NEXT_PUBLIC_SUPABASE_URL!,19 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,20 {21 cookies: {22 getAll() { return cookieStore.getAll() },23 setAll(cookiesToSet) {24 cookiesToSet.forEach(({ name, value, options }) =>25 cookieStore.set(name, value, options)26 )27 },28 },29 }30 )31}Expected result: Sessions persist across page reloads in SSR apps because the token is stored in cookies that both the server and client can read.
Add Next.js middleware to refresh sessions on navigation
Add Next.js middleware to refresh sessions on navigation
In Next.js App Router, server components cannot set cookies directly. You need middleware that runs on every request to refresh expired sessions and update the cookie. Without this middleware, the session cookie will expire and users will appear logged out even though their refresh token is still valid. Create a middleware.ts file in your project root that creates a Supabase server client and calls getUser() to trigger token refresh.
1// middleware.ts (project root)2import { createServerClient } from '@supabase/ssr'3import { NextResponse, type NextRequest } from 'next/server'45export async function middleware(request: NextRequest) {6 let response = NextResponse.next({ request })78 const supabase = createServerClient(9 process.env.NEXT_PUBLIC_SUPABASE_URL!,10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,11 {12 cookies: {13 getAll() { return request.cookies.getAll() },14 setAll(cookiesToSet) {15 cookiesToSet.forEach(({ name, value }) =>16 request.cookies.set(name, value)17 )18 response = NextResponse.next({ request })19 cookiesToSet.forEach(({ name, value, options }) =>20 response.cookies.set(name, value, options)21 )22 },23 },24 }25 )2627 await supabase.auth.getUser()28 return response29}3031export const config = {32 matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],33}Expected result: Session tokens are refreshed on every page navigation, and users stay logged in across server-rendered pages.
Complete working example
1// Complete Supabase client setup for session persistence2// Works for client-side rendered apps (Vite, CRA)34import { createClient } from '@supabase/supabase-js'56const supabaseUrl = import.meta.env.VITE_SUPABASE_URL7const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY89if (!supabaseUrl || !supabaseAnonKey) {10 throw new Error(11 'Missing Supabase environment variables. ' +12 'Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.'13 )14}1516// Create a SINGLE client instance — never call createClient() in a component17export const supabase = createClient(supabaseUrl, supabaseAnonKey, {18 auth: {19 // Persist session in localStorage (default behavior)20 persistSession: true,21 // Automatically refresh tokens before they expire22 autoRefreshToken: true,23 // Detect session from URL (needed for magic links and OAuth)24 detectSessionInUrl: true,25 },26})2728// Helper to get the current session (use in client-side code only)29export async function getCurrentSession() {30 const { data: { session }, error } = await supabase.auth.getSession()31 if (error) {32 console.error('Failed to get session:', error.message)33 return null34 }35 return session36}3738// Helper to get the verified user (safe for authorization checks)39export async function getCurrentUser() {40 const { data: { user }, error } = await supabase.auth.getUser()41 if (error) {42 console.error('Failed to get user:', error.message)43 return null44 }45 return user46}4748// Register this in your root component to keep auth state in sync49export function onAuthChange(50 callback: (event: string, session: any) => void51) {52 const { data: { subscription } } = supabase.auth.onAuthStateChange(callback)53 return () => subscription.unsubscribe()54}Common mistakes when debugging Supabase Auth Not Persisting Session
Why it's a problem: Creating a new Supabase client in every React component instead of sharing one instance
How to avoid: Create the client once in a dedicated file (lib/supabase.ts) and import it everywhere. Each createClient() call creates an independent session store.
Why it's a problem: Using getSession() on the server to verify authentication, which reads unverified data from cookies
How to avoid: Always use getUser() for server-side auth checks. It makes an API request to validate the JWT, while getSession() only reads the cached token.
Why it's a problem: Missing the Next.js middleware that refreshes session cookies on navigation
How to avoid: Create a middleware.ts file in the project root that calls supabase.auth.getUser() on every request. Without this, cookies expire and sessions appear lost.
Why it's a problem: Setting persistSession to false accidentally, which disables localStorage persistence
How to avoid: Check your createClient options. The default is persistSession: true. If you set it to false for testing, the session will only last until the page is closed.
Best practices
- Use a singleton pattern for the Supabase client — one shared instance per context (browser or server)
- Always register onAuthStateChange in your root component to handle TOKEN_REFRESHED and SIGNED_OUT events
- Use @supabase/ssr for any SSR framework (Next.js, SvelteKit, Nuxt) instead of the base supabase-js client
- Add middleware in Next.js to refresh tokens on every request so server components always have a valid session
- Never trust getSession() for authorization — it reads from local storage and can be spoofed. Use getUser() on the server.
- Store Supabase URL and anon key in environment variables, never hardcode them
- Test session persistence in incognito mode to catch storage issues early
- Log auth events during development to see exactly when sessions are created, refreshed, and destroyed
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Supabase auth session disappears every time I refresh the page in my React app. I am using @supabase/supabase-js v2 with createClient. Walk me through every possible cause and how to fix each one, including the client setup, onAuthStateChange, and localStorage verification.
Debug why my Supabase session is not persisting. Check that I have a singleton client, onAuthStateChange registered, and correct cookie config for SSR. Show the complete setup for a Next.js App Router project using @supabase/ssr.
Frequently asked questions
Why does my Supabase session disappear after page refresh?
The most common cause is creating multiple Supabase client instances instead of sharing one. Each createClient() call has its own session store. Create the client once in a shared file and import it everywhere.
What is the difference between getSession() and getUser()?
getSession() reads the cached token from local storage or cookies without verifying it. getUser() makes an API call to Supabase to validate the JWT. Always use getUser() for security-critical checks on the server.
Do I need @supabase/ssr for a client-side only React app?
No. If your app is entirely client-rendered (Vite, CRA), the base @supabase/supabase-js client stores sessions in localStorage automatically. You only need @supabase/ssr for server-rendered frameworks like Next.js or SvelteKit.
How long does a Supabase session last before expiring?
The default JWT expiry is 3600 seconds (1 hour). The client automatically refreshes the token using the refresh token before it expires. You can change the JWT expiry in Dashboard > Authentication > Settings.
Why does my session work in development but not in production?
Check your environment variables. If NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY are missing in production, the client cannot initialize. Also verify that your production domain is listed in Authentication > URL Configuration in the Dashboard.
Can I store the Supabase session in a cookie instead of localStorage?
Yes. Use @supabase/ssr, which stores the session in cookies. This is required for SSR frameworks and also works for client-side apps. Cookie-based storage is generally more secure than localStorage because cookies can be httpOnly.
Can RapidDev help me debug authentication issues in my Supabase project?
Yes. RapidDev's engineering team specializes in Supabase auth configuration, including SSR session management, JWT handling, and complex auth flows across multiple frameworks.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation