Back to blog
·6 min read

Why Your Supabase + Next.js App Logs Users Out in Production (And How to Fix It)

Your Supabase authentication works flawlessly on localhost but mysteriously logs users out after a few hours in production. This race condition between Edge, Server, and Client is costing teams revenue. Here's exactly how to fix it.

Why Your Supabase + Next.js App Logs Users Out in Production

You launch your Next.js application backed by Supabase. Everything works perfectly during testing. Users can log in, access protected routes, and interact with their data. Then production happens.

Reports start coming in: users are being randomly logged out. The UI shows them as guests. They refresh the page and suddenly they're logged back in. This isn't a flaky test environment or a misconfiguration specific to your setup. This is the most common and most expensive failure point in Supabase + Next.js applications, and it stems from a fundamental race condition between three different execution layers: the Edge, the Server, and the Client.

The problem isn't Supabase. The problem isn't Next.js. The problem is how these two systems communicate about authentication state when you're not careful about session management.

The Root Cause: The Session Desync Race Condition

Supabase handles authentication using HTTP-only cookies. These cookies are secure, cannot be accessed by JavaScript, and are automatically included in every request. This is good for security. It's bad for the race condition we're about to discuss.

Here's what happens in a typical Supabase + Next.js App Router setup:

Your middleware runs at the Edge and checks if the user's auth cookie is valid. If it's expired or invalid, the middleware refreshes the session using Supabase. This updated cookie gets sent back to the browser.

Meanwhile, your Server Components are running on the origin server, caching authentication state aggressively for performance. They pull user data from the database based on the auth context they see at render time.

Then your Client Components run in the browser. They make requests to API routes or use client-side Supabase queries. But they're working with a stale version of the auth state because the middleware refresh and the server-side cache aren't synchronized.

The user looks logged out because the client has old session data. Refresh the page, and the new request hits the middleware fresh, re-establishes the correct session, and suddenly they're logged in again.

This isn't a race condition that shows up on localhost because you're usually the only one accessing your app, timing is predictable, and you're not dealing with real network latency. In production with multiple concurrent users and geographic distribution, the timing windows where this race condition manifests become much wider.

Debugging the Problem: What to Look For

If you suspect you're experiencing session desync, look for these patterns:

  • Users are logged out in the UI but can still access protected routes if they navigate directly
  • Redirects to login happen inconsistently
  • The browser console shows auth state mismatches
  • Refreshing the page fixes the problem temporarily
  • This happens more frequently during peak traffic times
  • To confirm, add logging to your middleware, server components, and client components. Log the session validity, the timestamp when it was last verified, and the cookie values. You'll usually see a clear gap where the middleware has refreshed the session, but the client is still working with the old one.

    The Fix: Proper Middleware Configuration

    The solution involves three parts: middleware configuration, server-side auth helpers, and a client-side AuthProvider that stays synchronized.

    First, your middleware needs to properly initialize and refresh the Supabase session before your application code runs:

    ```

    import { createServerClient } from '@supabase/ssr'

    import { NextResponse, type NextRequest } from 'next/server'

    export async function middleware(request: NextRequest) {

    let response = NextResponse.next({

    request: {

    headers: request.headers,

    },

    })

    const supabase = createServerClient(

    process.env.NEXT_PUBLIC_SUPABASE_URL!,

    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,

    {

    cookies: {

    getAll() {

    return request.cookies.getAll()

    },

    setAll(cookiesToSet) {

    cookiesToSet.forEach(({ name, value, options }) =>

    response.cookies.set(name, value, options)

    )

    },

    },

    }

    )

    const { data: { user } } = await supabase.auth.getUser()

    return response

    }

    export const config = {

    matcher: [

    '/((?!_next/static|_next/image|favicon.ico|.*\\.png|.*\\.jpg).*)',

    ],

    }

    ```

    This middleware runs at the Edge on every request before your application code. It explicitly refreshes the Supabase session and ensures the cookie is current. The getAll and setAll functions make sure any session updates are reflected in the response sent to the browser.

    Client-Side Session Synchronization

    On the client side, create an AuthProvider that listens to Supabase auth state changes and keeps your app synchronized:

    ```

    'use client'

    import { createContext, useContext, useEffect, useState } from 'react'

    import { createBrowserClient } from '@supabase/ssr'

    const AuthContext = createContext(null)

    export function AuthProvider({ children }) {

    const [supabase] = useState(() =>

    createBrowserClient(

    process.env.NEXT_PUBLIC_SUPABASE_URL!,

    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

    )

    )

    const [user, setUser] = useState(null)

    const [loading, setLoading] = useState(true)

    useEffect(() => {

    const {

    data: { subscription },

    } = supabase.auth.onAuthStateChange((event, session) => {

    setUser(session?.user ?? null)

    setLoading(false)

    })

    return () => subscription?.unsubscribe()

    }, [supabase])

    return (

    <AuthContext.Provider value={{ supabase, user, loading }}>

    {children}

    </AuthContext.Provider>

    )

    }

    export function useAuth() {

    const context = useContext(AuthContext)

    if (!context) {

    throw new Error('useAuth must be used within AuthProvider')

    }

    return context

    }

    ```

    The onAuthStateChange subscription ensures that whenever Supabase detects a session change (like a refresh from the middleware), your client components are notified immediately. This closes the gap between server and client state.

    Why This Matters for Production Applications

    This isn't just a technical detail. When users get logged out randomly, they lose trust in your application. They might abandon critical tasks. In SaaS applications, session instability directly impacts churn and support costs.

    Building production-ready applications with proper authentication is precisely the kind of complexity that slows down development. This is why frameworks and scaffolds matter. When you're using structured templates that bake in proper authentication patterns, you eliminate entire classes of bugs before they make it to production.

    Tools like ZipBuild handle these session management patterns automatically in the generated scaffold, letting you focus on your actual business logic instead of debugging race conditions under deadline pressure.

    Testing Your Fix

    After implementing proper middleware and the AuthProvider, test with this scenario:

  • Log in to your application
  • Open DevTools and go to Application > Cookies
  • Wait for the session to refresh (usually happens during normal requests)
  • Watch the session cookie change
  • Verify that your client-side auth state updates immediately
  • Refresh the page and confirm the user is still logged in
  • The session should stay fresh and synchronized across all three layers: Edge, Server, and Client.

    Key Takeaways

    The Supabase + Next.js logout problem happens because session state can desynchronize between your middleware, server components, and client components. The fix requires explicit session refresh in middleware and synchronized auth state on the client side. This is a solvable problem, but it requires understanding how these layers communicate.

    If you're building a Supabase + Next.js application and want these patterns baked in from the start, try the free discovery chat at zipbuild.dev.

    Written by ZipBuild Team

    Ready to build with structure?

    Try the free discovery chat and see how ZipBuild architects your idea.

    Start Building