Next.js + Supabase: The Complete Integration Guide for 2026
Everything you need to wire Supabase into a Next.js App Router application — auth, database queries, Row Level Security, and real-time subscriptions.
Why Supabase + Next.js
Supabase gives you a Postgres database, auth, storage, and real-time subscriptions with a generous free tier and an excellent TypeScript SDK. Paired with Next.js App Router, you get a full-stack application with SSR, edge-ready API routes, and zero backend server to manage.
Installation
npm install @supabase/supabase-js @supabase/ssr
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
Client Setup
Supabase provides two client utilities for Next.js App Router — one for the browser, one for the server.
// lib/supabase/client.ts — browser client
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts — server client (reads cookies)
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: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}
Authentication
Use Supabase Auth with server-side session management via cookies.
// app/login/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
export default function LoginPage() {
const supabase = createClient()
async function signInWithGithub() {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: `${location.origin}/auth/callback` },
})
}
return <button onClick={signInWithGithub}>Sign in with GitHub</button>
}
// app/auth/callback/route.ts — exchanges the code for a session
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')
if (code) {
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(`${origin}/dashboard`)
}
Protecting Pages with Middleware
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { /* same cookie helpers */ } }
)
const { data: { user } } = await supabase.auth.getUser()
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}
export const config = { matcher: ['/dashboard/:path*'] }
Querying the Database
In Server Components, use the server client directly:
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: projects, error } = await supabase
.from('projects')
.select('id, name, created_at')
.order('created_at', { ascending: false })
if (error) throw error
return <ProjectList projects={projects} />
}
Row Level Security
Always enable RLS on every table. Define policies that restrict data access to the authenticated user.
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Users can only see their own projects
CREATE POLICY "users can view own projects"
ON projects FOR SELECT
USING (auth.uid() = user_id);
-- Users can only insert their own projects
CREATE POLICY "users can insert own projects"
ON projects FOR INSERT
WITH CHECK (auth.uid() = user_id);
With RLS in place, the anon key is safe to use on the client — Supabase enforces access at the database level.
Real-Time Subscriptions
For live-updating UIs (dashboards, notifications), subscribe to database changes:
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function LiveProjectList({ initialProjects }: { initialProjects: Project[] }) {
const [projects, setProjects] = useState(initialProjects)
const supabase = createClient()
useEffect(() => {
const channel = supabase
.channel('projects-changes')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'projects',
}, (payload) => {
if (payload.eventType === 'INSERT') {
setProjects((prev) => [payload.new as Project, ...prev])
}
})
.subscribe()
return () => { supabase.removeChannel(channel) }
}, [supabase])
return <ProjectList projects={projects} />
}
Type Safety with Generated Types
Generate TypeScript types from your Supabase schema:
npx supabase gen types typescript --project-id yourprojectid > src/types/supabase.ts
Then use them throughout your application:
import type { Database } from '@/types/supabase'
type Project = Database['public']['Tables']['projects']['Row']
Common Pitfalls
Using the service role key on the client. The service role key bypasses RLS entirely. Keep it server-side only, never in NEXT_PUBLIC_ env vars.
Forgetting to refresh sessions. Use the middleware pattern above to refresh the session cookie on every request — otherwise users get logged out unexpectedly.
Skipping RLS. Disabling RLS during development and forgetting to re-enable it is a critical security hole. Enable RLS on every table before launch.
Supabase and Next.js are a natural pair — you get the ergonomics of a managed backend with the performance of a React Server Component–first architecture.
Continue reading
Related articles
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringNext.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources