JSTAcademy
0 XP
Dashboard
Crash Courses
Wiring Next.js + Supabase on Vercel
15 min
Masters+150 XP
Crash Courses · Masters

Wiring Next.js + Supabase on Vercel

Auth, data fetching, and environment variables connected end-to-end — the exact setup that works in production.
15 min read+150 XP on completionCert: Ship It Crash Course
Tap any word in the text below to start reading from there.

Wiring Next.js + Supabase on Vercel

The Full Auth Architecture

Browser → Next.js Middleware (check session cookie)
              ↓ authenticated         ↓ unauthenticated
          Protected page          Redirect → /login
              ↓
         Server Component (read session, fetch user data)
              ↓
         Supabase (RLS uses auth.uid() from session)

---

Step 1: Install Dependencies

npm install @supabase/ssr @supabase/supabase-js

---

Step 2: Create Supabase Clients

// src/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!
  )
}
// src/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() { return cookieStore.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}

---

Step 3: Middleware — Protect Routes

// middleware.ts (root of project)
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, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()
  const path = request.nextUrl.pathname

  // Redirect unauthenticated users away from protected routes
  if (!user && path.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Redirect authenticated users away from auth pages
  if (user && (path === '/login' || path === '/signup')) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return supabaseResponse
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}

---

Step 4: Server Component with Auth

// src/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')

  // RLS uses auth.uid() = user.id — only fetches this user's courses
  const { data: courses } = await supabase
    .from('courses')
    .select('*')
    .order('created_at', { ascending: false })

  return (
    <main>
      <h1>Welcome, {user.email}</h1>
      <ul>
        {courses?.map(c => <li key={c.id}>{c.title}</li>)}
      </ul>
    </main>
  )
}

---

Vercel Deployment Checklist

Before pushing:

# 1. Type check
npx tsc --noEmit

# 2. Test build locally
npm run build

# 3. Check .gitignore includes .env.local
cat .gitignore | grep env

# 4. Verify Vercel has all env vars set
# Dashboard → Settings → Environment Variables
0%