How to Fix Random Logout Issues in Next.js and Supabase Production Apps
Your users are getting mysteriously logged out after a few hours in production, but refreshing magically logs them back in. This is a classic authentication race condition between Next.js middleware, server components, and the client. Here's how to fix it.
The Production Authentication Bug That Costs You Days of Debugging
You've deployed your Next.js and Supabase app to production. Everything works great locally. Then your first real users start reporting that they're randomly logged out—sometimes after an hour, sometimes after three. But here's the weird part: when they refresh the page, they're logged back in. The session was never actually invalid.
This isn't a Supabase bug. It's not a Next.js bug either. It's a race condition between three different layers of your application: the Edge (where Next.js middleware runs), the Server (where your page and API routes execute), and the Client (where your UI lives and reads auth state).
If you're building with Supabase and Next.js, this is one of the most common and most expensive failure points you'll hit before launch. It's also completely preventable once you understand the underlying issue.
Why This Happens: The Three-Layer Authentication Race
Here's what's actually happening when your user gets mysteriously logged out:
Your Supabase session is stored in an HttpOnly cookie. When the user's session expires (or a token refresh fails silently), that cookie becomes invalid. But your Next.js middleware, server components, and client-side auth state aren't always in sync about whether the session is actually valid.
The race condition typically looks like this:
The problem is that you have multiple sources of truth for whether the user is actually authenticated, and they're not communicating fast enough.
The Fix: Establish a Single Source of Truth
The solution involves three changes to how you handle authentication:
### 1. Move Auth State to Server-First Architecture
Stop relying on client-side auth state as your source of truth. Instead, let your server (via Next.js middleware and server components) own the authentication state.
Your middleware should check the session cookie on every request:
```
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createMiddlewareClient({ req: request, res: response })
// This refreshes the session if the refresh token is valid
// If not, it clears the cookies
const { data: { session } } = await supabase.auth.getSession()
// If there's no session and they're trying to access a protected route
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}
```
This runs on every request, so your server always knows the real state of the session before the page renders.
### 2. Use Server Components for Auth-Protected Content
In your pages, fetch the session server-side in your server component. Don't wait for the client to figure out auth:
```
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const supabase = createServerComponentClient({ cookies })
const {
data: { session },
} = await supabase.auth.getSession()
if (!session) {
redirect('/login')
}
// Now render dashboard knowing user IS authenticated
return <Dashboard user={session.user} />
}
```
This means your page won't even render to the browser if the user isn't authenticated. There's no flash of logged-out state.
### 3. Add Client-Side Session Refresh Listeners
On the client side, listen for auth changes and revalidate your app state immediately when they happen:
```
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSupabaseClient } from '@supabase/auth-helpers-react'
export function SessionListener() {
const router = useRouter()
const supabase = useSupabaseClient()
useEffect(() => {
// Listen for auth state changes (logout, token refresh, etc)
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
// Revalidate server state immediately
router.refresh()
// If session ended, redirect to login
if (event === 'SIGNED_OUT' || !session) {
router.push('/login')
}
}
)
return () => subscription?.unsubscribe()
}, [supabase.auth, router])
return null
}
```
This component should wrap your entire app (add it to your root layout). Now when the session changes—whether in this tab or another tab—your app knows about it immediately.
The Architecture Pattern You Need
The mental model that prevents this bug is simple:
This pattern prevents the race condition because there's no window where the server and client disagree about whether the user is authenticated.
Enable Row Level Security While You're At It
While you're fixing authentication, add one more critical protection: enable RLS on your Supabase tables. A surprising number of production apps launch without this enabled, which means your database is completely exposed if someone steals a session token.
In Supabase, go to each table and enable RLS. Then add a basic policy:
```
CREATE POLICY "Users can only read their own data"
ON your_table
FOR SELECT
USING (auth.uid() = user_id);
```
This ensures that even if someone gets a valid session token, they can only read their own data.
AI-Assisted Development and Authentication
When you're using Claude Code or another AI assistant to build this auth layer, the key is giving it the right constraints. Rather than pasting a custom prompt into every session, document your authentication patterns in a CLAUDE.md file at your project root:
```
# Authentication Rules
```
This way, when you ask Claude Code to add a new protected page or API route, it will follow your established patterns automatically, preventing the same bugs from appearing in different parts of your app.
The Faster Way Forward
Building authentication correctly in Next.js and Supabase requires understanding these three layers and how they communicate. It's absolutely doable, but it's also one of the most common places where architects and developers make expensive mistakes.
If you're building a full-stack SaaS application and want to avoid spending days debugging authentication issues, ZipBuild's scaffolding includes production-ready authentication patterns with this exact architecture pre-configured, so your team can focus on your business logic instead of authentication edge cases.
Try the free discovery chat at zipbuild.dev to see how a properly structured auth layer can save your team weeks of development time.
Written by ZipBuild Team
Ready to build with structure?
Try the free discovery chat and see how ZipBuild architects your idea.
Start Building