Documentation Index Fetch the complete documentation index at: https://mintlify.com/supabase/supabase/llms.txt
Use this file to discover all available pages before exploring further.
Learn how to integrate Supabase with Next.js using the App Router, Server Components, and Server Actions for a modern, full-stack development experience.
Why Next.js + Supabase?
Next.js and Supabase work perfectly together:
Server Components for secure data fetching
Server Actions for mutations
Middleware for auth protection
Edge Runtime support
Type safety with TypeScript
Quick Start
Create a Next.js App
npx create-next-app@latest my-app
cd my-app
Choose:
TypeScript: Yes
App Router: Yes
Tailwind CSS: Yes (recommended)
Install Supabase
npm install @supabase/supabase-js @supabase/ssr
Set Up Environment Variables
Create .env.local: NEXT_PUBLIC_SUPABASE_URL = your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY = your-anon-key
Create Supabase Clients
Create lib/supabase/client.ts for Client Components: import { createBrowserClient } from '@supabase/ssr'
export function createClient () {
return createBrowserClient (
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY !
)
}
Create lib/supabase/server.ts for Server Components: 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 {
// Server Component - ignore
}
},
},
}
)
}
Create lib/supabase/middleware.ts for Middleware: import { createServerClient } from '@supabase/ssr'
import { NextResponse , type NextRequest } from 'next/server'
export async function updateSession ( 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 , options }) => request . cookies . set ( name , value ))
supabaseResponse = NextResponse . next ({
request ,
})
cookiesToSet . forEach (({ name , value , options }) =>
supabaseResponse . cookies . set ( name , value , options )
)
},
},
}
)
// Refresh session if expired
const {
data : { user },
} = await supabase . auth . getUser ()
return supabaseResponse
}
Set Up Middleware
Create middleware.ts in your project root: import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware ( request : NextRequest ) {
return await updateSession ( request )
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!_next/static|_next/image|favicon.ico|.* \\ .(?:svg|png|jpg|jpeg|gif|webp)$).*)' ,
],
}
Usage Patterns
Server Components (Recommended)
Fetch data securely in Server Components:
import { createClient } from '@/lib/supabase/server'
export default async function Page () {
const supabase = await createClient ()
const { data : posts } = await supabase
. from ( 'posts' )
. select ( '*' )
. order ( 'created_at' , { ascending: false })
return (
< div >
< h1 > Posts </ h1 >
< ul >
{ posts ?. map (( post ) => (
< li key = {post. id } > {post. title } </ li >
))}
</ ul >
</ div >
)
}
Client Components
Use Client Components for interactivity:
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect , useState } from 'react'
export default function ClientComponent () {
const [ data , setData ] = useState < any []>([])
const supabase = createClient ()
useEffect (() => {
const fetchData = async () => {
const { data } = await supabase . from ( 'posts' ). select ( '*' )
setData ( data || [])
}
fetchData ()
}, [ supabase ])
return (
< div >
{ data . map (( item ) => (
< div key = {item. id } > {item. title } </ div >
))}
</ div >
)
}
Server Actions
Handle mutations with Server Actions:
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost ( formData : FormData ) {
const supabase = await createClient ()
const title = formData . get ( 'title' ) as string
const content = formData . get ( 'content' ) as string
const { data : { user } } = await supabase . auth . getUser ()
if ( ! user ) {
redirect ( '/login' )
}
const { error } = await supabase
. from ( 'posts' )
. insert ({ title , content , user_id: user . id })
if ( error ) {
throw new Error ( error . message )
}
revalidatePath ( '/posts' )
redirect ( '/posts' )
}
export async function deletePost ( postId : string ) {
const supabase = await createClient ()
const { error } = await supabase
. from ( 'posts' )
. delete ()
. eq ( 'id' , postId )
if ( error ) {
throw new Error ( error . message )
}
revalidatePath ( '/posts' )
}
Use in components:
import { createPost } from './actions'
export default function CreatePostForm () {
return (
< form action = { createPost } >
< input name = "title" placeholder = "Title" required />
< textarea name = "content" placeholder = "Content" required />
< button type = "submit" > Create Post </ button >
</ form >
)
}
Route Handlers
Create API routes:
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET ( request : Request ) {
const supabase = await createClient ()
const { data , error } = await supabase
. from ( 'posts' )
. select ( '*' )
if ( error ) {
return NextResponse . json ({ error: error . message }, { status: 500 })
}
return NextResponse . json ({ data })
}
export async function POST ( request : Request ) {
const supabase = await createClient ()
const body = await request . json ()
const { data , error } = await supabase
. from ( 'posts' )
. insert ( body )
. select ()
. single ()
if ( error ) {
return NextResponse . json ({ error: error . message }, { status: 500 })
}
return NextResponse . json ({ data })
}
Authentication
Sign Up/Sign In
Create app/login/actions.ts:
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function login ( formData : FormData ) {
const supabase = await createClient ()
const data = {
email: formData . get ( 'email' ) as string ,
password: formData . get ( 'password' ) as string ,
}
const { error } = await supabase . auth . signInWithPassword ( data )
if ( error ) {
redirect ( '/error' )
}
revalidatePath ( '/' , 'layout' )
redirect ( '/' )
}
export async function signup ( formData : FormData ) {
const supabase = await createClient ()
const data = {
email: formData . get ( 'email' ) as string ,
password: formData . get ( 'password' ) as string ,
}
const { error } = await supabase . auth . signUp ( data )
if ( error ) {
redirect ( '/error' )
}
revalidatePath ( '/' , 'layout' )
redirect ( '/' )
}
Protected Routes
Protect pages in Server Components:
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function ProtectedPage () {
const supabase = await createClient ()
const { data : { user } } = await supabase . auth . getUser ()
if ( ! user ) {
redirect ( '/login' )
}
return < div > Protected content for {user. email } </ div >
}
OAuth Providers
Sign in with OAuth:
'use client'
import { createClient } from '@/lib/supabase/client'
export default function OAuthButtons () {
const supabase = createClient ()
const signInWithGitHub = async () => {
await supabase . auth . signInWithOAuth ({
provider: 'github' ,
options: {
redirectTo: ` ${ location . origin } /auth/callback` ,
},
})
}
return (
< button onClick = { signInWithGitHub } >
Sign in with GitHub
</ button >
)
}
Create callback handler at app/auth/callback/route.ts:
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET ( request : Request ) {
const { searchParams , origin } = new URL ( request . url )
const code = searchParams . get ( 'code' )
const next = searchParams . get ( 'next' ) ?? '/'
if ( code ) {
const supabase = await createClient ()
const { error } = await supabase . auth . exchangeCodeForSession ( code )
if ( ! error ) {
return NextResponse . redirect ( ` ${ origin }${ next } ` )
}
}
return NextResponse . redirect ( ` ${ origin } /auth/auth-code-error` )
}
Real-time Subscriptions
Subscribe to changes in Client Components:
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect , useState } from 'react'
export default function RealtimeComponent () {
const [ messages , setMessages ] = useState < any []>([])
const supabase = createClient ()
useEffect (() => {
// Load initial data
const loadMessages = async () => {
const { data } = await supabase . from ( 'messages' ). select ( '*' )
setMessages ( data || [])
}
loadMessages ()
// Subscribe to changes
const channel = supabase
. channel ( 'messages' )
. on (
'postgres_changes' ,
{ event: 'INSERT' , schema: 'public' , table: 'messages' },
( payload ) => {
setMessages (( current ) => [ ... current , payload . new ])
}
)
. subscribe ()
return () => {
supabase . removeChannel ( channel )
}
}, [ supabase ])
return (
< div >
{ messages . map (( msg ) => (
< div key = {msg. id } > {msg. content } </ div >
))}
</ div >
)
}
Type Safety
Generate types from your database:
npx supabase gen types typescript --project-id your-project-ref > lib/database.types.ts
Use types:
import { Database } from '@/lib/database.types'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient < Database >(
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY !
)
type Post = Database [ 'public' ][ 'Tables' ][ 'posts' ][ 'Row' ]
Best Practices
Use Server Components Fetch data in Server Components when possible for better security and performance.
Server Actions for Mutations Use Server Actions instead of Route Handlers for form submissions and mutations.
Client Components for Interactivity Use Client Components only when you need hooks, event handlers, or browser APIs.
Middleware for Auth Use middleware to refresh auth sessions automatically.
Type Generation Generate TypeScript types from your database schema.
Next Steps
Build a Todo App Follow our step-by-step tutorial
User Management Add user profiles and avatars
File Uploads Integrate Supabase Storage
Realtime Chat Build a chat application