Fix Supabase Auth Session Not Persisting After Refresh
technology

Fix Supabase Auth Session Not Persisting After Refresh

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

2026-02-16
8 min read
Fix Supabase Auth Session Not Persisting After Refresh

Fix Supabase Auth Session Not Persisting After Refresh#

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:

// 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.
          }
        },
      },
    }
  )
}

Step 2: Create Client-Side Supabase Client#

Create a separate client for browser-side operations:

// 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:

// 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:

// 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:

'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:

// 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#

// 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.

FAQ#

Why does my session disappear after refresh?#

This happens when Next.js Server Components don't have access to the session cookies. The solution is to create a proper server-side Supabase client that reads cookies using Next.js's cookies() function and implement middleware to refresh sessions.

How often should I refresh the session?#

Supabase automatically refreshes sessions when they're about to expire (default is 1 hour). The middleware handles this automatically on every request, so you don't need to manually refresh sessions.

What's the difference between server and client Supabase clients?#

The server client uses Next.js's cookies() function to read/write cookies and works in Server Components. The browser client uses browser APIs and works in Client Components. You need both for a complete Next.js 14 App Router application.

Can I use localStorage instead of cookies?#

While you can use localStorage, cookies are recommended because they work with Server Components, are more secure (httpOnly), and automatically handle session refresh. localStorage only works on the client side.

Do I need to install additional packages?#

Yes, you need @supabase/ssr package which provides the createServerClient and createBrowserClient functions. Install it with: npm install @supabase/ssr

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