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:
- 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.
}
},
},
}
)
}
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
Related Articles
- Supabase Auth Redirect Not Working Next.js App Router Solution
- Handle Supabase Auth Errors in Next.js Middleware
- Implement Supabase Magic Link Auth in Next.js 15
- Build Protected Routes in Next.js 15 with Supabase
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.
Originally published at https://iloveblogs.blog
Top comments (0)