Supabase Auth Session Disappears on Refresh? Fix (2026)
Supabase

Supabase Auth Session Disappears on Refresh? Fix (2026)

Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.

Updated
8 min read
Supabase Auth Session Disappears on Refresh? Fix (2026)

Photo by FlyD on Unsplash

Fix Supabase Auth Session Not Persisting After Refresh Next.js 14#

Supabase auth sessions mysteriously disappearing after page refresh is one of the most frustrating issues indie developers face when building Next.js applications. You implement authentication, everything works perfectly—until the user refreshes the page and suddenly they're logged out. As covered in our Supabase Authentication & Authorization Patterns, authentication is critical to any application, and session persistence is fundamental to good user experience.

In this guide, I'll show you exactly why this happens and how to fix it in 5 minutes with tested code examples that work with Next.js 14 and Supabase v2.

Why Supabase Sessions Disappear After Refresh#

The root cause is how Next.js handles server-side rendering and client-side hydration. When you refresh a page in Next.js 14 with the App Router, the following happens:

  1. Server renders the page - The server doesn't have access to browser cookies by default
  2. Client hydrates - React takes over on the client side
  3. Session check fails - If the session isn't properly passed from server to client, it appears lost

The issue occurs because Supabase stores session tokens in cookies, but Next.js Server Components don't automatically read these cookies unless you explicitly configure them to do so.

Common Misconceptions#

  • "Supabase is broken" - No, it's a Next.js configuration issue
  • "I need to use localStorage" - Actually, cookies are more secure and work better with SSR
  • "This only happens in development" - It happens in production too if not fixed properly

Step-by-Step Solution#

Step 1: Create Server-Side Supabase Client#

First, create a server-side Supabase client that properly reads cookies:

typescript
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
 
export function createClient() {
  const cookieStore = cookies()
 
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}

If you have upgraded to Next.js 15 or 16, the cookies() call above must be awaited: const cookieStore = await cookies(). Without the await, the cookie store resolves to undefined, getUser() returns null, and the session vanishes on refresh even though the cookie is present in the request — the exact symptom this article fixes. The async migration (including the codemod that rewrites most call sites automatically) is covered in the cookies() should be awaited fix guide.

Step 2: Create Client-Side Supabase Client#

Create a separate client for browser-side operations:

typescript
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
 
export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Step 3: Add Middleware for Session Refresh#

Create middleware to automatically refresh sessions on every request:

typescript
// middleware.ts
import { createServerClient, type CookieOptions } 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: {
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value,
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name,
            value,
            ...options,
          })
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: '',
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name,
            value: '',
            ...options,
          })
        },
      },
    }
  )
 
  // Refresh session if expired - required for Server Components
  await supabase.auth.getUser()
 
  return response
}
 
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Step 4: Use Server Client in Server Components#

In your Server Components, use the server client:

typescript
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export default async function DashboardPage() {
  const supabase = createClient()
  
  const { data: { user } } = await supabase.auth.getUser()
  
  if (!user) {
    redirect('/login')
  }
 
  return (
    <div>
      <h1>Welcome, {user.email}</h1>
      <p>Your session persists across refreshes!</p>
    </div>
  )
}

Step 5: Use Client Client in Client Components#

In Client Components, use the browser client:

typescript
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
 
export function UserProfile() {
  const [user, setUser] = useState(null)
  const supabase = createClient()
 
  useEffect(() => {
    const getUser = async () => {
      const { data: { user } } = await supabase.auth.getUser()
      setUser(user)
    }
 
    getUser()
 
    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null)
      }
    )
 
    return () => subscription.unsubscribe()
  }, [])
 
  if (!user) return <div>Loading...</div>
 
  return <div>Logged in as: {user.email}</div>
}

Working Code Examples#

Complete Authentication Flow#

Here's a complete example showing sign-in with session persistence:

typescript
// app/login/page.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
 
export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()
 
  async function handleLogin(e: React.FormEvent) {
    e.preventDefault()
    setLoading(true)
 
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })
 
    if (error) {
      alert(error.message)
      setLoading(false)
      return
    }
 
    // Session is automatically stored in cookies
    router.push('/dashboard')
    router.refresh() // Refresh to update Server Components
  }
 
  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  )
}

Protected Route Pattern#

typescript
// app/protected/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export default async function ProtectedPage() {
  const supabase = createClient()
  
  const { data: { user } } = await supabase.auth.getUser()
  
  if (!user) {
    redirect('/login')
  }
 
  // User is authenticated, session persists
  return (
    <div>
      <h1>Protected Content</h1>
      <p>This page requires authentication</p>
      <p>User: {user.email}</p>
    </div>
  )
}

Common Mistakes#

  • Mistake #1: Using only client-side Supabase client - This doesn't work with Server Components. You need both server and client implementations.

  • Mistake #2: Not implementing middleware - Without middleware, sessions won't refresh automatically and will expire, causing users to be logged out.

  • Mistake #3: Mixing server and client clients - Always use the server client in Server Components and the browser client in Client Components. Mixing them causes hydration errors.

  • Mistake #4: Not calling router.refresh() after login - After authentication, you need to refresh the router to update Server Components with the new session.

  • Mistake #5: Forgetting to handle cookie errors - The try-catch blocks in the server client are essential because Server Components can't set cookies directly.

Conclusion#

Session persistence in Next.js 14 with Supabase requires proper setup of both server and client Supabase clients, plus middleware to handle session refresh. The key is understanding that Server Components need explicit cookie access, which the @supabase/ssr package provides.

Follow the steps above, and your users will stay logged in across page refreshes. The middleware automatically handles session refresh, and the dual-client approach works seamlessly with Next.js's hybrid rendering model.

Test your implementation by logging in, refreshing the page, and verifying the user remains authenticated. If you encounter issues, double-check that your middleware is running and that you're using the correct client in each context.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.