$ cat /posts/supabase-with-nextjs-15-complete-integration-guide.md
[tags]Supabase

Supabase with Next.js 15: Complete Integration Guide

drwxr-xr-x2026-01-255 min0 views
Supabase with Next.js 15: Complete Integration Guide

Next.js 15 with App Router and Server Components provides the perfect framework for building production-ready full-stack applications with Supabase, combining server-side rendering, static generation, client-side interactivity, and optimized performance. This comprehensive integration guide covers installing Supabase in Next.js 15, configuring server and client components, implementing authentication with middleware, protecting routes, creating API routes with Edge Functions, using Server Actions for mutations, optimizing data fetching with server components, implementing real-time features, deploying to Vercel, and building a complete blog application demonstrating all patterns. Unlike client-only React apps, Next.js with Supabase enables SEO-friendly pages, faster initial loads through server rendering, secure API routes, and automatic code splitting. The combination of Next.js App Router's React Server Components and Supabase's backend-as-a-service creates powerful, scalable applications with minimal configuration. Before proceeding, understand Supabase client basics, authentication, and Row Level Security.

Why Next.js with Supabase?

FeatureBenefitUse Case
Server ComponentsFetch data on server, reduce client bundleSEO-friendly pages
Server ActionsServer-side mutations without API routesForm submissions
MiddlewareProtect routes before renderingAuth checks
Static GenerationPre-render pages at build timeBlogs, docs
API RoutesCustom backend endpointsWebhooks, integrations

Installation and Setup

bashsetup.sh
# Create Next.js 15 app
npx create-next-app@latest my-supabase-app
# Choose: App Router, TypeScript (optional), Tailwind CSS

cd my-supabase-app

# Install Supabase packages
npm install @supabase/supabase-js @supabase/ssr

# Create environment file
# .env.local
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

# Project structure
# app/
#   layout.js
#   page.js
#   login/
#     page.js
#   dashboard/
#     page.js
# lib/
#   supabase/
#     client.js
#     server.js
#     middleware.js
# middleware.js

Creating Supabase Clients

javascriptsupabase_clients.js
// lib/supabase/client.js - Client Component 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.js - Server Component client
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: {
        get(name) {
          return cookieStore.get(name)?.value
        },
        set(name, value, options) {
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // Handle cookie errors in middleware
          }
        },
        remove(name, options) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
            // Handle cookie errors in middleware
          }
        },
      },
    }
  )
}

Authentication Middleware

javascriptmiddleware.js
// middleware.js - Protect routes and refresh session
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'

export async function middleware(request) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        get(name) {
          return request.cookies.get(name)?.value
        },
        set(name, value, options) {
          request.cookies.set({ name, value, ...options })
          response = NextResponse.next({
            request: { headers: request.headers },
          })
          response.cookies.set({ name, value, ...options })
        },
        remove(name, options) {
          request.cookies.set({ name, value: '', ...options })
          response = NextResponse.next({
            request: { headers: request.headers },
          })
          response.cookies.set({ name, value: '', ...options })
        },
      },
    }
  )

  // Refresh session if needed
  const { data: { user } } = await supabase.auth.getUser()

  // Protect dashboard routes
  if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Redirect logged-in users away from login
  if (request.nextUrl.pathname === '/login' && user) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return response
}

export const config = {
  matcher: ['/dashboard/:path*', '/login']
}

Server Component Data Fetching

javascriptserver_component.js
// app/dashboard/page.js - Server Component
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default async function Dashboard() {
  const supabase = await createClient()

  // Get authenticated user
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  // Fetch data on server (SEO-friendly, faster initial load)
  const { data: posts } = await supabase
    .from('posts')
    .select('*')
    .eq('user_id', user.id)
    .order('created_at', { ascending: false })

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {user.email}</p>

      <h2>Your Posts ({posts?.length || 0})</h2>
      <ul>
        {posts?.map(post => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
          </li>
        ))}
      </ul>
    </div>
  )
}

// This page is dynamically rendered on each request
export const dynamic = 'force-dynamic'

Client Component with Real-time

javascriptclient_component.js
// app/dashboard/posts-client.js - Client Component with real-time
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export default function PostsClient({ initialPosts }) {
  const [posts, setPosts] = useState(initialPosts)
  const supabase = createClient()

  useEffect(() => {
    // Subscribe to real-time changes
    const channel = supabase
      .channel('posts-changes')
      .on(
        'postgres_changes',
        { event: '*', schema: 'public', table: 'posts' },
        (payload) => {
          if (payload.eventType === 'INSERT') {
            setPosts(prev => [payload.new, ...prev])
          } else if (payload.eventType === 'UPDATE') {
            setPosts(prev => prev.map(post =>
              post.id === payload.new.id ? payload.new : post
            ))
          } else if (payload.eventType === 'DELETE') {
            setPosts(prev => prev.filter(post => post.id !== payload.old.id))
          }
        }
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [])

  return (
    <div>
      <h2>Live Posts ({posts.length})</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
          </li>
        ))}
      </ul>
    </div>
  )
}

Server Actions for Mutations

javascriptserver_actions.js
// app/dashboard/actions.js - Server Actions
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function createPost(formData) {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return { error: 'Unauthorized' }
  }

  const title = formData.get('title')
  const content = formData.get('content')

  const { data, error } = await supabase
    .from('posts')
    .insert([{ title, content, user_id: user.id }])
    .select()
    .single()

  if (error) {
    return { error: error.message }
  }

  revalidatePath('/dashboard')
  return { success: true, data }
}

export async function deletePost(postId) {
  const supabase = await createClient()

  const { error } = await supabase
    .from('posts')
    .delete()
    .eq('id', postId)

  if (error) {
    return { error: error.message }
  }

  revalidatePath('/dashboard')
  return { success: true }
}

// Use in component:
// <form action={createPost}>
//   <input name="title" />
//   <textarea name="content" />
//   <button type="submit">Create Post</button>
// </form>

Login Page with Authentication

javascriptlogin_page.js
// app/login/page.js - Login with Server Actions
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default function LoginPage() {
  async function login(formData) {
    'use server'

    const email = formData.get('email')
    const password = formData.get('password')

    const supabase = await createClient()

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })

    if (error) {
      redirect('/login?error=Invalid credentials')
    }

    redirect('/dashboard')
  }

  async function signup(formData) {
    'use server'

    const email = formData.get('email')
    const password = formData.get('password')

    const supabase = await createClient()

    const { error } = await supabase.auth.signUp({
      email,
      password,
    })

    if (error) {
      redirect('/login?error=Signup failed')
    }

    redirect('/login?message=Check your email to verify')
  }

  return (
    <div className="login-page">
      <h1>Login</h1>
      
      <form action={login}>
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Log In</button>
      </form>

      <form action={signup}>
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Sign Up</button>
      </form>
    </div>
  )
}

API Route Example

javascriptapi_route.js
// app/api/posts/route.js - API Route
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET(request) {
  const supabase = await createClient()

  const { data, error } = await supabase
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ posts: data })
}

export async function POST(request) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { title, content } = await request.json()

  const { data, error } = await supabase
    .from('posts')
    .insert([{ title, content, user_id: user.id }])
    .select()
    .single()

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ post: data })
}

Deployment to Vercel

bashdeployment.sh
# Deploy to Vercel
npm install -g vercel
vercel login
vercel

# Or connect GitHub repo in Vercel dashboard
# 1. Push to GitHub
# 2. Import project in Vercel
# 3. Add environment variables:
#    NEXT_PUBLIC_SUPABASE_URL
#    NEXT_PUBLIC_SUPABASE_ANON_KEY
# 4. Deploy

# Set up custom domain in Vercel settings
# Configure redirect URL in Supabase:
# Dashboard > Authentication > URL Configuration
# Add: https://yourdomain.com/auth/callback

Next.js + Supabase Best Practices

  • Use Server Components: Fetch data on server for SEO, faster loads, and reduced client bundle size
  • Implement Middleware: Protect routes and refresh sessions before page renders
  • Server Actions for Mutations: Use Server Actions instead of API routes for simpler form handling
  • Client Components for Interactivity: Use 'use client' only when needed for real-time, state, or browser APIs
  • Enable RLS: Always implement Row Level Security policies for data protection
  • Revalidate Paths: Use revalidatePath() after mutations to update server-rendered content
  • Environment Variables: Never expose service_role key in client code
Production Ready: This integration pattern is production-tested and powers thousands of Next.js + Supabase apps. Combine with real-time features, file storage, and Edge Functions for complete applications.

Next Steps

  1. Add Advanced Features: Implement real-time subscriptions and file uploads
  2. Build Complete Apps: Apply patterns from React todo tutorial in Next.js
  3. Learn Migrations: Manage schema changes with database migrations
  4. Optimize Performance: Use static generation, image optimization, and caching strategies

Conclusion

Integrating Supabase with Next.js 15 combines the best of both platforms—Next.js's server rendering, routing, and optimizations with Supabase's authentication, database, storage, and real-time features. The App Router's Server Components enable SEO-friendly pages with efficient data fetching, while Server Actions simplify mutations without dedicated API routes. Middleware provides route protection and session management before rendering, ensuring secure, fast applications. By using server components for data fetching, client components for interactivity, and Row Level Security for data protection, you build production-ready full-stack applications with minimal configuration. Always remember to protect routes with middleware, use appropriate client types (server vs browser), implement RLS policies, and leverage Server Actions for form handling. With this integration mastered, you're equipped to build scalable, secure, SEO-friendly applications combining Next.js and Supabase's full capabilities. Continue exploring with Edge Functions and migrations for complete production workflows.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.