Skip to main content

Fixing "AuthSessionMissingError" in Next.js 15 with Supabase

 You have built a seamless authentication flow on your local machine. You can sign up, sign in, and access protected routes on localhost:3000. The moment you deploy to Vercel, Netlify, or a Docker container, the authentication breaks.

Users are stuck in a redirect loop, constantly bouncing between /dashboard and /login. Checking your server logs reveals the dreaded AuthSessionMissingError or persistent 401 Unauthorized responses, even immediately after a "successful" login.

This discrepancy between development and production environments is the single most common frustration when integrating Next.js 15 with Supabase. Here is exactly why it breaks and the code required to fix it.

The Root Cause: The "Cookie Hand-Off" Failure

To understand the fix, you must understand the failure mechanism. Supabase authentication relies on JSON Web Tokens (JWTs). For security and UX, these tokens are stored in HTTP-only cookies.

On localhost, browsers are lenient. They ignore strict cookie policies like Secure (HTTPS only) and often permit lax SameSite attributes. However, production environments enforce these strictly.

Furthermore, Next.js 15’s App Router introduces an architectural complexity: Server Components are read-only regarding cookies.

When a user refreshes a page:

  1. The request hits the Next.js Middleware.
  2. Supabase attempts to refresh the session if the token is close to expiry.
  3. The Failure Point: If your middleware does not explicitly capture the refreshed cookies from Supabase and pass them to the outgoing response, the browser never receives the new token.

The user has a valid session in the database, but their browser is holding a stale cookie. Next.js sees an invalid token and redirects them.

The Solution: Implementing Robust Session Management

To fix this, we must upgrade from the legacy auth-helpers (if used) to the modern @supabase/ssr package and implement a middleware strategy that acts as a "Cookie Bridge."

Prerequisites

Ensure you have the latest SSR package installed:

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

1. The Server Client Utility

In Next.js 15, cookies() is an asynchronous function. We need a utility that handles the cookie store correctly for both Server Components and Server Actions.

Create utils/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 {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}

2. The Middleware Bridge (Critical Fix)

This is where the production issue is usually solved. We must create a separate utility to update the session within the middleware context. Middleware in Next.js runs on the Edge runtime and handles Request/Response objects differently than Node.js runtimes.

Create utils/supabase/middleware.ts:

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  // Create an unmodified response
  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) {
          // 1. Set cookies on the request (so Server Components allow access)
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          )
          
          // 2. Refresh the response object
          response = NextResponse.next({
            request,
          })
          
          // 3. Set cookies on the response (so the browser updates)
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // This will refresh the session if needed
  const {
    data: { user },
  } = await supabase.auth.getUser()

  // PROTECTED ROUTE LOGIC
  // Customize this list for your specific protected paths
  if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Update the response with the new cookies
  return response
}

Now, wire this into your root middleware.ts:

import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * Feel free to modify this pattern to include more paths.
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Why This Fix Works

The code above solves the "AuthSessionMissingError" by addressing the lifecycle of the HTTP request:

  1. Request Copying: We create a response object immediately.
  2. Dual Setting: When Supabase refreshes the token inside setAll, we apply the new cookie to both the incoming request (so the Server Component waiting downstream sees the authenticated user) and the outgoing response (so the browser persists the new token).
  3. Secure Verification: We use supabase.auth.getUser() rather than getSession()getUser sends a request to the Supabase Auth server to validate the JWT, whereas getSession simply decodes the cookie locally (which can be spoofed or stale).

Production Pitfalls and Edge Cases

Even with the code above, configuration errors can trigger 401 loops.

1. Environment Variables Mismatch

In Vercel, ensure your environment variables are set for the Production environment, not just Preview or Development.

  • NEXT_PUBLIC_SUPABASE_URL: Must be the full URL (e.g., https://xyz.supabase.co).
  • NEXT_PUBLIC_SUPABASE_ANON_KEY: Ensure you aren't using the Service Role key by mistake.

2. Prefetching and Middleware

Next.js <Link> components prefetch routes by default. This triggers the middleware. If your middleware logic is too heavy or has side effects (like database writes on every hit), your application performance will degrade. Ensure your middleware only handles Authentication and Redirection. Data fetching should happen in Server Components.

3. IPv6 Issues (Docker/Node)

If you are deploying to a custom Docker container (not Vercel) and see timeouts, Node.js 18+ favors IPv6. If your Supabase instance doesn't support IPv6 or your network isn't configured for it, force IPv4 by setting the environment variable: NODE_OPTIONS="--dns-result-order=ipv4first"

Summary

The transition to Next.js 15 requires strict adherence to cookie handling protocols. The "AuthSessionMissingError" is rarely a bug in Supabase itself, but rather a disconnect between the Edge Middleware and the Browser's cookie jar.

By utilizing @supabase/ssr and manually handling the cookie copy process in middleware, you ensure that the authentication state is synchronized across the Client, the Edge, and the Server.