How to Fix Supabase Auth Logout Issues in Next.js 15: The Session Persistence Problem Solved
Your Supabase-backed Next.js app logs users out after a few hours, even though authentication works locally. This guide reveals the session persistence problem and the exact fix developers need to keep users authenticated in production.
The Problem: Users Getting Logged Out Without Warning
You build a Next.js 15 app with Supabase auth. Everything works perfectly in development. You deploy to production and within hours, support tickets roll in: "Why does it log me out?" or worse, "I'm logged out but the login page keeps redirecting me back."
The UI shows guest status. The user refreshes. Suddenly they're authenticated again. This happens randomly every few hours.
This is one of the most common production issues developers face when combining Supabase with Next.js App Router, and the fix isn't documented clearly anywhere. It's not actually about Supabase breaking—it's about how Next.js handles session state across server and client boundaries.
Why This Happens: The Auth State Mismatch
When you initialize Supabase client-side, it stores the session in localStorage (browser) and potentially in cookies. But here's the problem: Next.js server components can't see localStorage, and if your cookie isn't configured correctly, the server has no idea who the user is.
This creates a state mismatch:
When the user navigates, the server renders a guest version of the page. The client rehydrates and sees the session in localStorage. The page flickers, redirects loop, or the user appears logged out until they refresh.
The core issue: you're relying on localStorage as your source of truth, but Next.js server components can't access it.
The Real Fix: Configure Supabase Sessions in Cookies
Supabase can store sessions in cookies instead of relying on localStorage alone. This solves the server-client mismatch because:
Here's the correct setup for Next.js 15 with App Router:
First, install the required package:
npm install @supabase/ssr
Create a utility file to initialize the Supabase client correctly:
// lib/supabase.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
getAll() {
// In browser context, use document.cookie
if (typeof document === 'undefined') return []
return document.cookie
.split('; ')
.map(c => {
const [name, ...rest] = c.split('=')
return { name, value: decodeURIComponent(rest.join('=')) }
})
},
setAll(cookies) {
// This tells Supabase to use cookies
if (typeof document === 'undefined') return
cookies.forEach(({ name, value, options }) => {
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${options.maxAge}`
})
},
},
}
)
}
The critical part: those cookie handlers tell Supabase to sync sessions via cookies, not just localStorage.
Sync Auth State Across Layouts
Next, create a provider component that runs early in your layout to refresh the auth session:
// app/providers.tsx
'use client'
import { useEffect } from 'react'
import { createClient } from '@/lib/supabase'
export function AuthProvider({ children }) {
const supabase = createClient()
useEffect(() => {
// Refresh session on mount
supabase.auth.getSession()
// Listen for auth changes and sync across tabs
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
// This runs when auth state changes
// The session is now synced via cookies
})
return () => {
subscription?.unsubscribe()
}
}, [supabase])
return <>{children}</>
}
Add this to your root layout:
// app/layout.tsx
import { AuthProvider } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
)
}
Use Server Components Correctly
For protected routes, fetch the user session server-side using the Supabase server client, not the browser client:
// lib/supabase-server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createServerClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookies) {
cookies.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options)
})
},
},
}
)
}
Now protect pages server-side:
// app/dashboard/page.tsx
import { createServerClient } from '@/lib/supabase-server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const supabase = createServerClient()
const { data } = await supabase.auth.getUser()
if (!data.user) {
redirect('/login')
}
return <div>Welcome {data.user.email}</div>
}
The server checks authentication before rendering. No flicker. No mismatch.
Three More Production Safeguards
Beyond session persistence, add these protections:
Enable CORS on your Supabase project for your exact domain, not wildcards. Set your session timeout to something reasonable (default is 1 hour). Most logout complaints are actually just expired sessions, not bugs.
Also, monitor your Supabase auth logs. Go to the Authentication section of your dashboard and check for failed session refreshes. If you see patterns, it usually points to cookie configuration issues.
Why AI-Assisted Development Makes This Worse
Here's what often happens: Claude Code generates a basic Supabase setup quickly. It works immediately. You ship it. Production breaks.
Why? Because AI assistants optimize for "working locally," not "production-ready." The default setup uses localStorage only. It's fewer lines of code. It feels complete because the demo works.
Real production software requires this session persistence layer, but AI won't generate it unless you specifically ask about server-side authentication or session management. This is why the feedback loop matters: generate, test in production-like conditions, then improve.
If you're building with AI assistance, always ask: "How does this handle session persistence across page reloads in production?" That single question would have prevented this entire class of bugs.
Tools like ZipBuild scaffold this session layer from the start—the cookie configuration, server client setup, and auth state sync are all pre-configured so you don't ship with the wrong defaults.
Quick Checklist
The logout problem disappears once sessions persist correctly. Your users stay logged in, and more importantly, they stay logged in across browser refreshes and tab switches.
Try the free discovery chat at zipbuild.dev to see how production-ready scaffolds handle auth from day one.
Written by ZipBuild Team
Ready to build with structure?
Try the free discovery chat and see how ZipBuild architects your idea.
Start Building