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.
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.
This page covers why the session disappears between requests and the cookie wiring that fixes it, with code for 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:
- Server renders the page - The server doesn't have access to browser cookies by default
- Client hydrates - React takes over on the client side
- 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.
}
},
},
}
)
}If you have upgraded to Next.js 15 or 16, the
cookies()call above must be awaited:const cookieStore = await cookies(). Without theawait, the cookie store resolves toundefined,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:
// 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!
)
}Why
getUser()and notgetSession()—getSession()reads the cookie as-is, so it cannot tell a valid token from a tampered one. Supabase's server-side Next.js guide is explicit: usegetClaimsto verify identity,getUserwhen you need a fresh user record confirmed by the Auth server, andgetSessiononly when you need the raw access or refresh token — not to decide who the caller is.
Step 3: Add Middleware for Session Refresh#
Create middleware to automatically refresh sessions on every request:
Next.js 16: the
middlewarefile convention is deprecated and renamedproxy.ts(export a function namedproxy).middleware.tsstill runs, and it stays the option if you need the Edge runtime, whichproxydoes not support (Next.js 16 upgrade guide). The code below is the same under either name.
// 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.
Missing Required Error Components, Refreshing…#
When a Supabase session vanishes on refresh, the browser console or server logs sometimes show a cryptic message like “missing required error components, refreshing…”. This isn’t a Supabase-authored error—it’s a framework-level warning that surfaces when the client-side hydration layer can’t find the pieces it needs to reconcile the server-rendered auth state with the browser’s cookie store. In practice, it means one of three required components is missing or misconfigured: the environment variables, the middleware that refreshes the token, or the cookie-handling logic inside your server client.
1. Missing environment variables#
The Supabase server client (Step 1 in the article) reads NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. If either is undefined—perhaps because you forgot to add them to .env.local or they’re not prefixed with NEXT_PUBLIC_—the client initialises with undefined values. The first server-side call to getUser() then fails silently, the session appears missing, and the framework may emit a “missing required” warning during the refresh cycle. Double-check your environment:
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...After adding them, restart the dev server (environment changes aren’t hot-reloaded). If you’re using a monorepo or a build tool that strips NEXT_PUBLIC_ variables, verify they’re actually exposed to the browser bundle.
2. Middleware not refreshing the session#
The middleware (Step 3) is the only place where the refresh token can be exchanged for a new access token on every request. Without it, the server-side client sees an expired token after the initial sign-in, and the session disappears. The “missing required” warning often appears because the middleware matcher excludes the route that needs the session, or the middleware file itself is absent. Ensure middleware.ts sits at the project root (or src/middleware.ts) and that its matcher covers your protected routes. The article’s matcher excludes static assets; if your dashboard lives under /app, that’s already covered.
3. Cookie handling errors in the server client#
The createServerClient helper in Step 1 wraps cookieStore.set and remove in try‑catch blocks because Server Components can’t set cookies. If you’ve omitted those try‑catch blocks, an unhandled error during a refresh can break the entire request, leaving the client-side hydration with no session data and triggering the “missing required” warning. The exact error might read “Auth session missing!”—a separate but related problem we cover in Fix AuthSessionMissingError: Auth session missing!. The fix is to keep the try‑catch blocks exactly as shown, and to ensure you’re using @supabase/ssr (not the older @supabase/auth-helpers-nextjs).
If you’re still seeing the warning after verifying these three components, check that you’re not mixing the server and browser clients (Mistake #3 in the article). The server client must only be used in Server Components; using it in a Client Component can cause hydration mismatches that the framework reports as missing required pieces.
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.
Further reading: Better Auth vs Supabase Auth 2026: Honest Verdict, Fix: Supabase redirectTo Ignored on Magic Link / Email Auth.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix Supabase: Invalid Refresh Token Already Used
Trace which client retains an old Supabase token, preserve refreshed response cookies, and let the affected browser sign in again without a global logout.
Fix: Supabase redirectTo Ignored on Magic Link / Email Auth
Magic links and password-reset emails that always land on the production Site URL instead of the redirectTo you passed. The allow-list mechanism behind it, and the wildcard pattern for local dev and preview branches.
Supabase getClaims() vs getSession(): The Silent Auth Bug
Your auth looks correct but users can bypass it. The bug: getSession() in server code reads from local storage without revalidating the token against the Auth server.


