TechLead
Lesson 9 of 22
5 min read
Supabase

Supabase with Next.js

Complete guide to integrating Supabase with Next.js App Router including SSR, server actions, and cookie-based auth

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/ssr instead 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

Continue Learning