Back to blog
·5 min read

How to Fix Supabase and Next.js Auth Session Desync in Production

Session desync between Next.js Server Components and Supabase auth is causing random logouts in production. Here's the exact fix startups are using to stop losing users.

The Problem: Your Users Are Getting Randomly Logged Out

You've shipped your Next.js SaaS. Users are signing up. Everything looks good in staging. Then production hits and support tickets start rolling in: "I keep getting logged out randomly." You check the code. The auth logic looks fine. But it's happening consistently—every few hours, authenticated users get kicked back to the login page.

This is the Supabase + Next.js session desync issue, and it's one of the most damaging authentication bugs you'll encounter because users don't just see an error message—they lose trust in your product.

The root cause is architectural: Next.js Server Components aggressively cache data to improve performance, but Supabase session tokens expire on a fixed schedule. When the cached auth state doesn't update, users appear logged in on the server while being logged out on Supabase's backend. The next request fails, and the session collapses.

This happens because:

  • Next.js caches function results across requests for the same user
  • Supabase tokens expire every hour (default)
  • The client never tells the server that the session has expired
  • Your app keeps serving the old cached session data
  • If you're building with AI assistants like Claude Code, this is exactly the kind of subtle infrastructure bug that's easy to miss because the logic looks correct in isolation—the problem emerges from how systems interact at scale.

    Why This Breaks Production Fast

    When authentication fails randomly, you're not losing a user for a day—you're losing them permanently. Users assume your app is buggy. They switch to a competitor. And you'll never know it was an auth issue because they don't report it.

    For startups, this is critical infrastructure. An authentication layer failure is the most expensive thing that can go wrong after data loss because it directly impacts user retention and trust.

    The fix requires understanding how Supabase auth actually works and rebuilding how your Next.js app listens to session state changes.

    The Production Fix: Global Session State Listener

    The solution involves three components:

    First, create a Client Component that subscribes to Supabase's auth state changes and updates your app globally. This runs in the browser and listens for actual token refresh events.

    Second, sync that state back to your server layer through a middleware that checks freshness on every request.

    Third, make sure your Server Components read from the current session, not cached values.

    Here's the implementation:

    Create a context provider for auth state:

    ```javascript

    "use client"

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

    import { createBrowserClient } from "@supabase/ssr"

    const AuthContext = createContext()

    export function AuthProvider({ children }) {

    const [session, setSession] = useState(null)

    const [isLoading, setIsLoading] = useState(true)

    const supabase = createBrowserClient(

    process.env.NEXT_PUBLIC_SUPABASE_URL,

    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

    )

    useEffect(() => {

    // Get initial session

    supabase.auth.getSession().then(({ data: { session } }) => {

    setSession(session)

    setIsLoading(false)

    })

    // Listen for auth changes

    const {

    data: { subscription },

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

    setSession(session)

    setIsLoading(false)

    // Refresh page on token change to clear server cache

    if (event === "TOKEN_REFRESHED" || event === "SIGNED_IN" || event === "SIGNED_OUT") {

    window.location.reload()

    }

    })

    return () => {

    subscription?.unsubscribe()

    }

    }, [supabase])

    return (

    <AuthContext.Provider value={{ session, isLoading }}>

    {children}

    </AuthContext.Provider>

    )

    }

    export function useAuth() {

    return useContext(AuthContext)

    }

    ```

    Wrap your root layout with this provider:

    ```javascript

    import { AuthProvider } from "@/providers/auth-provider"

    export default function RootLayout({ children }) {

    return (

    <html>

    <body>

    <AuthProvider>

    {children}

    </AuthProvider>

    </body>

    </html>

    )

    }

    ```

    Create middleware that validates session freshness on every request:

    ```javascript

    import { createServerClient } from "@supabase/ssr"

    import { NextResponse } from "next/server"

    export async function middleware(request) {

    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)

    })

    },

    },

    }

    )

    // Refresh session on every request

    await supabase.auth.getSession()

    return response

    }

    export const config = {

    matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],

    }

    ```

    The key insight: The page reload on TOKEN_REFRESHED forces Next.js to clear its cache and request fresh data from Supabase. This feels crude but it's what production apps are doing because it's reliable.

    Why This Matters for AI-Assisted Development

    When you're building with Claude Code or similar AI assistants, they generate correct code that follows patterns. But authentication layer interactions are systems-level problems that require understanding how caching, token expiration, and client-server synchronization interact.

    The best AI assistance catches these at architecture time, not after they've burned production. Tools like ZipBuild scaffold production-ready authentication layers that have this session management pattern baked in from day one, so you avoid discovering this bug when users are experiencing it.

    Testing Before Production

    Test this locally by:

  • Setting your Supabase session timeout to 5 minutes
  • Wait longer than the timeout in your app
  • Verify you get logged out cleanly without errors
  • Check that the page reload happens automatically
  • If random logouts continue, your middleware isn't running on every request. Check your matcher config.

    The most important part: Don't skip the onAuthStateChange listener. That's the actual connection between your app and Supabase's real auth state. Without it, you're just reading cookies.

    The Bigger Pattern

    This pattern applies beyond just Supabase. Whenever you have:

  • Cached server state
  • Client-side expiring tokens
  • Users who stay on the same page for hours
  • You need an explicit mechanism for the client to tell the server "my token refreshed." Not hoping the cache expires naturally. Not polling every 30 seconds. A real event listener that says: "the token changed, clear everything and start fresh."

    Building this correctly from the start saves you from the nightmare of debugging random logouts at 2am when your app has 10k active users.

    Try the free discovery chat at zipbuild.dev to see how production-ready scaffolds handle authentication from the beginning.

    Written by ZipBuild Team

    Ready to build with structure?

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

    Start Building