Back to blog
·6 min read

Why Your Supabase + Next.js Users Keep Getting Logged Out in Production (And How to Fix It)

Session desync between Next.js middleware and Server Components causes random logouts in production. We walk through the root cause and the step-by-step fix that keeps your Supabase auth working reliably.

The Silent Production Killer: Supabase Sessions Desynchronizing

You deployed your Next.js app to production last week. Everything works perfectly on localhost. Your auth flow is clean, protected routes render correctly, and the user experience feels solid.

Then users start reporting they're getting logged out randomly.

The strange part: they refresh the page and suddenly they're logged back in. Or they click a protected link, get bounced to login, and immediately get redirected back to the page they wanted. It's inconsistent enough that it's not obviously broken—it's just broken enough to destroy user trust and waste support time.

If you're building on Supabase with Next.js App Router, you've likely hit this. It's the most expensive authentication bug in production Next.js applications right now, and it stems from a fundamental mismatch between how Next.js caches data and how Supabase manages session state.

The Root Cause: Server Component Caching vs. Session Refresh

Here's what's happening under the hood:

Next.js Server Components aggressively cache data by default. When a component renders, its data is cached for the duration of the request, and often beyond it (especially with Incremental Static Regeneration). This is great for performance, but catastrophic for authentication state.

Your middleware refreshes the auth cookie correctly—it pulls a fresh session from Supabase, updates the cookie, and continues. But your Server Components are still holding a reference to the old session object from the cache. The user's browser has the updated cookie, but your server is serving stale auth data.

When the client makes a navigation request, Next.js sometimes serves cached Server Component data instead of re-rendering. The user appears logged out in the UI because the cached component thinks the session is invalid. But the middleware still has the fresh cookie, so a page refresh triggers a re-render that reads the correct session.

This mismatch is invisible to you until it hits production, where caching strategies are aggressive and concurrent users trigger cache inconsistencies.

The Deprecated Packages Making It Worse

Many existing guides tell you to use auth-helpers-nextjs or raw cookie manipulation methods. These packages and patterns:

  • Don't properly sync between middleware and Server Components
  • Fail to handle cookie refresh timing correctly
  • Create authentication loops where the middleware refreshes the session but the client never sees it
  • Introduce security vulnerabilities by exposing session tokens in ways that don't follow current best practices
  • If you're using these, your logout issue is almost certainly happening.

    The Solution: @supabase/ssr and Proper Session Sync

    Supabase released the @supabase/ssr package specifically to solve this problem. It handles the session refresh pattern correctly and ensures your middleware, Server Components, and Client Components all see the same auth state.

    Here's the correct setup:

    ### Step 1: Install the Right Package

    ```

    npm install @supabase/ssr @supabase/supabase-js

    ```

    ### Step 2: Create a Middleware That Refreshes the Session

    Create lib/supabase/middleware.ts:

    ```

    import { createServerClient } from '@supabase/ssr'

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

    export async function updateSession(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()

    if (

    !user &&

    !request.nextUrl.pathname.startsWith('/auth') &&

    !request.nextUrl.pathname.startsWith('/login')

    ) {

    const url = request.nextUrl.clone()

    url.pathname = '/login'

    return NextResponse.redirect(url)

    }

    return response

    }

    ```

    This middleware runs on every request and refreshes the session from Supabase. The key detail: it sets the refreshed cookies on the response, so the client gets the updated session data.

    ### Step 3: Wire It Into Your Middleware

    Create middleware.ts in your project root:

    ```

    import { type NextRequest } from 'next/server'

    import { updateSession } from '@/lib/supabase/middleware'

    export async function middleware(request: NextRequest) {

    return await updateSession(request)

    }

    export const config = {

    matcher: [

    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',

    ],

    }

    ```

    ### Step 4: Create a Server-Side Auth Helper

    Create lib/supabase/server.ts:

    ```

    import { createServerClient } from '@supabase/ssr'

    import { cookies } from 'next/headers'

    export async function createClient() {

    const cookieStore = await cookies()

    return createServerClient(

    process.env.NEXT_PUBLIC_SUPABASE_URL,

    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,

    {

    cookies: {

    getAll() {

    return cookieStore.getAll()

    },

    setAll(cookiesToSet) {

    try {

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

    cookieStore.set(name, value, options)

    )

    } catch {

    // Cookies can't be modified in some contexts

    }

    },

    },

    }

    )

    }

    ```

    ### Step 5: Use the Helper in Your Server Components

    ```

    import { createClient } from '@/lib/supabase/server'

    import { redirect } from 'next/navigation'

    export default async function Dashboard() {

    const supabase = await createClient()

    const {

    data: { user },

    } = await supabase.auth.getUser()

    if (!user) {

    redirect('/login')

    }

    return <div>Welcome {user.email}</div>

    }

    ```

    The critical pattern here: every Server Component that needs auth gets a fresh Supabase client instance. It reads the cookies set by the middleware (which just refreshed them), so it always has the current session state.

    Why This Works

    The middleware runs first and refreshes the session cookie on every request. Server Components then read those refreshed cookies. The client sees the updated auth state without any caching mismatches. There's no desync because the source of truth (the cookies) is updated before the components read it.

    Additional Production Safeguards

    Once you have session sync working, add these:

  • Set explicit cache headers: Add revalidate = 0 to components that depend on auth state
  • Use react-query or SWR on the client to refetch user data on visibility changes
  • Implement session timeout warnings so users expect logouts after inactivity
  • Monitor auth failures in your logs to catch any remaining edge cases early
  • Scaling This Pattern

    If you're building a more complex application with multiple auth-dependent features, this pattern scales. The same middleware and server client approach works whether you have 10 pages or 100. The Supabase SSR package handles the session refresh lifecycle automatically.

    Many teams discover this problem after launch when session desync affects real users. Catch it before that happens. If you're starting a new Next.js + Supabase project, use @supabase/ssr from day one.

    If you're uncertain about whether your current auth setup is following these patterns, ZipBuild can scaffold a production-ready Next.js project with Supabase authentication configured correctly, including proper session sync, role-based access control, and error handling—so you skip the month of debugging session issues and start shipping features.

    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