All insights
Engineering11 min read

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

code
npm install @supabase/supabase-js @supabase/ssr
code
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.

code
// 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!
  )
}
code
// 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.

code
// 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>
}
code
// 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

code
// 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:

code
// 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.

code
-- 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:

code
'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:

code
npx supabase gen types typescript --project-id yourprojectid > src/types/supabase.ts

Then use them throughout your application:

code
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.

Stay informed

Get our monthly deep dives.

Engineering, design, and growth insights — once a month. No spam.

Browse all resources