Back to blog
·6 min read

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:

  • User's session token expires in the background
  • Client-side code tries to fetch auth state and gets a 401 from Supabase
  • Your middleware clears the session cookie (correctly)
  • But your UI was already rendered with the old auth state cached
  • User sees themselves as logged out, even though the server-side session is already gone
  • They refresh, middleware re-checks auth, and now everything is consistent again
  • 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:

  • **Server owns auth state** (middleware + server components know first)
  • **Server tells client when state changes** (auth listeners trigger router.refresh())
  • **Client never trusts old auth state** (always revalidate on user interaction)
  • 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

  • Always use middleware to check session on protected routes
  • Use server components for auth-protected pages
  • Never trust client-side auth state as the source of truth
  • Always include SessionListener in root layout
  • Enable RLS on all tables
  • ```

    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