Supabase with Next.js App Router
Next.js is the most popular React framework for building full-stack web applications, and Supabase
provides first-class support through the @supabase/ssr package. This guide covers the complete
integration with the App Router, including server components, client components, middleware, and server actions.
🚀 What You'll Learn
- @supabase/ssr: The official package for server-side Supabase
- Server vs Client: Creating the right client for each context
- Middleware: Refreshing auth sessions on every request
- Server Actions: Mutating data securely from the server
Installation & Setup
npm install @supabase/supabase-js @supabase/ssr
# Environment variables in .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Creating the Supabase Client
Server Component Client
Server components need a client that reads cookies for the current user session.
// lib/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 is called from a Server Component
// This can be ignored if middleware refreshes sessions
}
},
},
}
)
}
Client Component Client
// 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!
)
}
Middleware for Auth Sessions
Middleware refreshes the auth token on every request, keeping the session alive.
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
)
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Refresh the auth session
await supabase.auth.getUser()
return supabaseResponse
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg)$).*)'],
}
SSR Data Fetching in Server Components
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')
const { data: projects } = await supabase
.from('projects')
.select('id, name, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
return (
<div>
<h1>Welcome, {user.email}</h1>
{projects?.map(project => (
<div key={project.id}>{project.name}</div>
))}
</div>
)
}
Server Actions with Supabase
// app/actions.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
export async function createProject(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Not authenticated')
const { error } = await supabase.from('projects').insert({
name: formData.get('name') as string,
user_id: user.id,
})
if (error) throw new Error(error.message)
revalidatePath('/dashboard')
}
⚠️ Important Note
Always use supabase.auth.getUser() in server code, not getSession().
The getUser() method validates the JWT with Supabase Auth, while getSession()
only reads the local cookie and can be spoofed.
💡 Key Takeaways
- • Use
@supabase/ssrinstead of the deprecated auth-helpers - • Create separate clients for server components and client components
- • Middleware refreshes auth sessions on every request
- • Always use
getUser()on the server for secure auth checks - • Server Actions provide a secure way to mutate data with Supabase
📚 Learn More
-
Next.js Quickstart →
Official Supabase Next.js quickstart with App Router.
-
Next.js Server-Side Auth →
Complete guide to server-side auth with Next.js.