Course navigationPhase 17
Volume 05 · Phase 17

Supabase Authentication

Integrate Supabase Auth into Next.js with correct browser/server clients, cookie/session behavior, sign-up/sign-in/sign-out, and protected routing.

15lessons + labs
5klesson words
15source references
What you will ship

Phase outcome and project increment

Learning outcome

Integrate Supabase Auth into Next.js with correct browser/server clients, cookie/session behavior, sign-up/sign-in/sign-out, and protected routing.

Concrete deliverable

Working TaskFlow authentication UI/session integration whose server-side checks are explicit and ready to hand tokens to NestJS.

Figure 17.1 — Where this phase lives in TaskFlow
auth form
Supabase Auth
session/cookies
Next.js server/client auth helpers
protected UI
access token handoff
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

Let Supabase Auth establish user identity while the application still owns TaskFlow authorization rules.

02

Keep service-role credentials off the browser and use public client credentials only for intended client operations.

03

Handle session restoration and expiration explicitly so UI state does not silently drift from server identity.

04

Verify authentication on the server for protected operations rather than trusting client-visible session state.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Configure project URLs/redirects and explain why local/preview/production values differ.

  2. 02

    Create browser and server Supabase clients with the correct key exposure model.

  3. 03

    Implement sign-up/sign-in and surface provider errors safely.

  4. 04

    Render authenticated state on the server rather than trusting only client state.

  5. 05

    Implement sign-out and prove protected routes recover to unauthenticated state.

  6. 06

    Inspect the session/access token and prepare the boundary for NestJS verification without exposing service-role credentials.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

Using service-role key in browser code

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 2

Relying only on client-side route hiding

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 3

Broken redirect URLs across environments

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 4

Stale UI after session changes

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Watch-order style

Lessons

The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.

01

Supabase Auth Architecture and Keys

Supabase Auth Architecture & Keys Place identity, sessions, keys, Next.js, and NestJS in the correct security boundaries Phase 17 - Supabase Authentication Volume 05 - Authentication & Secur

2 min
02

Dashboard URLs Email Confirmation and PKCE

Dashboard URLs, Email Confirmation & SSR Flow Configure the identity provider so the code has a correct redirect and confirmation environment Phase 17 - Supabase Authentication Volume 05 - A

2 min
03

Install and Create Browser Server Clients

Install & Create Browser/Server Clients Lock the 2026 packages and build the two SSR client boundaries correctly Phase 17 - Supabase Authentication Volume 05 - Authentication & Security Arch

2 min
04

Next.js 16 Proxy Session Refresh and getClaims

Next.js 16 Proxy, Session Refresh & getClaims Keep SSR cookies synchronized and verify identity at the request boundary Phase 17 - Supabase Authentication Volume 05 - Authentication & Securi

2 min
05

Signup and Email Confirmation

Signup & Email Confirmation Handle pending confirmation explicitly and exchange the email token hash for a session Phase 17 - Supabase Authentication Volume 05 - Authentication & Security Ar

2 min
06

SignIn SignOut and Session Lifecycle

Sign-In, Sign-Out & Session Lifecycle Create and terminate real Supabase sessions without confusing UI state with security state Phase 17 - Supabase Authentication Volume 05 - Authentication

2 min
07

Protected Server Components and Authenticated UI

Protected Server Components & Authenticated UI Render authenticated pages from verified request identity, not client-side guesses Phase 17 - Supabase Authentication Volume 05 - Authenticatio

2 min
08

getClaims getUser getSession and Access Token Handoff

getClaims, getUser, getSession & API Token Handoff Use each Supabase Auth method for its intended trust job and prepare the NestJS boundary Phase 17 - Supabase Authentication Volume 05 - Aut

2 min
09

SSR Caching Refresh and Auth Debugging

SSR Caching, Refresh & Auth Debugging Diagnose session failures without weakening cookie or token security Phase 17 - Supabase Authentication Volume 05 - Authentication & Security Architectu

2 min
10

Security Boundaries and NestJS Handoff

Security Boundaries & NestJS Handoff Define exactly what Phase 17 protects and what remains untrusted until Phase 18 Phase 17 - Supabase Authentication Volume 05 - Authentication & Security

2 min
11

Hands On Lab and Phase Exit

Hands-On Lab & Phase Exit Prove signup, confirmation, session refresh, route protection, and sign-out before protecting the API Phase 17 - Supabase Authentication Volume 05 - Authentication

2 min
LAB

Guided Build Lab

Purpose Integrate Supabase Auth into Next.js with correct browser/server clients, cookie/session behavior, sign-up/sign-in/sign-out, and protected routing.

3 min
LAB

Debugging Workshop

Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.

2 min
LAB

Phase Synthesis & Independent Challenge

Mission Rebuild the capability of Phase 17 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 17 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Configure project URLs/redirects and explain why local/preview/production values differ.

Practice

Implement sign-up/sign-in and surface provider errors safely.

Debug

reproduce and diagnose — Using service-role key in browser code.

Independent

deliver Working TaskFlow authentication UI/session integration whose server-side checks are explicit and ready to hand tokens to NestJS.

Explain-back

Can distinguish public and secret Supabase keys and Can create browser/server clients.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 17's engineering goal?
02Which practice best matches Phase 17's engineering goal?
03Which practice best matches Phase 17's engineering goal?
04Which practice best matches Phase 17's engineering goal?
05You are working through the Phase 17 lab. Which next action is the most disciplined step?
06You are working through the Phase 17 lab. Which next action is the most disciplined step?
07You are working through the Phase 17 lab. Which next action is the most disciplined step?
08You are working through the Phase 17 lab. Which next action is the most disciplined step?
Source snapshot

Reference files

Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.

reference/TaskFlow_Supabase_Auth_Snapshot/.env.local.example
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_REPLACE_ME
NEXT_PUBLIC_SITE_URL=http://localhost:3000
reference/TaskFlow_Supabase_Auth_Snapshot/AUTH_DASHBOARD_SETUP.md
# Supabase Auth Dashboard Setup - TaskFlow Phase 17

1. Authentication > Providers > Email: keep Email enabled.
2. Keep email confirmation enabled for the production-style learning flow.
3. Authentication > URL Configuration:
   - Site URL: http://localhost:3000 for local development.
   - Add production/staging redirect URLs before deployment.
4. Authentication > Email Templates > Confirm signup:

   Replace the confirmation link target with:

   {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email

5. Never place a service-role or secret API key in NEXT_PUBLIC_* variables.
6. Phase 17 uses the project's Publishable key in the browser and SSR clients.
reference/TaskFlow_Supabase_Auth_Snapshot/PHASE_17_TEST_MATRIX.md
# Phase 17 Test Matrix

- Unauthenticated GET /account -> redirected to /login.
- Unauthenticated GET /tasks -> redirected to /login.
- Sign up with valid email/password and email confirmation enabled -> no immediate session; message asks user to check email.
- Confirmation link -> /auth/confirm verifies token_hash and stores cookie-backed session.
- Confirmed user signs in -> redirected to /account.
- Refresh /account -> remains signed in.
- Wait/force access-token refresh path -> proxy keeps cookies synchronized.
- Sign out -> current browser session cookies cleared and /account redirects to /login.
- Invalid confirmation link -> /auth/error.
- Server protection uses getClaims(), not getSession().user.
- Service-role/secret key is absent from browser code and NEXT_PUBLIC_* variables.
reference/TaskFlow_Supabase_Auth_Snapshot/install-auth.ps1
# Run from: taskflow\apps\web
pnpm add @supabase/supabase-js@2.116.0 @supabase/ssr@0.12.7
reference/TaskFlow_Supabase_Auth_Snapshot/proxy.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/proxy'

export async function proxy(request: NextRequest) {
  return updateSession(request)
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
reference/TaskFlow_Supabase_Auth_Snapshot/src/app/account/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export const dynamic = 'force-dynamic'

export default async function AccountPage() {
  const supabase = await createClient()
  const { data, error } = await supabase.auth.getClaims()

  if (error || !data?.claims) {
    redirect('/login')
  }

  return (
    <main>
      <h1>TaskFlow account</h1>
      <p>Authenticated subject: {String(data.claims.sub)}</p>

      <form action="/auth/signout" method="post">
        <button type="submit">Sign out</button>
reference/TaskFlow_Supabase_Auth_Snapshot/src/app/auth/confirm/route.ts
import { type EmailOtpType } from '@supabase/supabase-js'
import { type NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const tokenHash = searchParams.get('token_hash')
  const type = searchParams.get('type') as EmailOtpType | null

  const redirectTo = request.nextUrl.clone()
  redirectTo.pathname = '/account'
  redirectTo.search = ''

  if (tokenHash && type) {
    const supabase = await createClient()
    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash: tokenHash,
    })
reference/TaskFlow_Supabase_Auth_Snapshot/src/app/auth/error/page.tsx
export default function AuthErrorPage() {
  return (
    <main>
      <h1>Authentication link could not be verified</h1>
      <p>Return to the sign-in page and request a new authentication flow.</p>
      <a href="/login">Back to sign in</a>
    </main>
  )
}
reference/TaskFlow_Supabase_Auth_Snapshot/src/app/auth/signout/route.ts
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'

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

  if (data?.claims) {
    // TaskFlow signs out only this browser session by default.
    await supabase.auth.signOut({ scope: 'local' })
  }

  revalidatePath('/', 'layout')
  return NextResponse.redirect(new URL('/login', request.url), {
    status: 303,
  })
}
reference/TaskFlow_Supabase_Auth_Snapshot/src/app/login/actions.ts
'use server'

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

function credentials(formData: FormData) {
  const email = String(formData.get('email') ?? '').trim()
  const password = String(formData.get('password') ?? '')

  if (!email || password.length < 8) {
    redirect('/login?error=Check+your+email+and+password')
  }

  return { email, password }
}

export async function login(formData: FormData) {
  const supabase = await createClient()
  const { error } = await supabase.auth.signInWithPassword(
reference/TaskFlow_Supabase_Auth_Snapshot/src/app/login/page.tsx
import { login, signup } from './actions'

export default async function LoginPage({
  searchParams,
}: {
  searchParams: Promise<{ error?: string; message?: string }>
}) {
  const params = await searchParams

  return (
    <main>
      <h1>TaskFlow</h1>
      <p>Sign in or create an account.</p>

      {params.error ? <p role="alert">{params.error}</p> : null}
      {params.message ? <p>{params.message}</p> : null}

      <form>
        <label>
          Email
reference/TaskFlow_Supabase_Auth_Snapshot/src/lib/supabase/access-token.ts
import { createClient } from '@/lib/supabase/server'

export async function getAccessTokenForApi() {
  const supabase = await createClient()
  const {
    data: { session },
  } = await supabase.auth.getSession()

  // getSession() is used here only because Phase 18 needs the raw token
  // to forward to NestJS. NestJS must verify the token before trusting it.
  return session?.access_token ?? null
}
reference/TaskFlow_Supabase_Auth_Snapshot/src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
  )
}
reference/TaskFlow_Supabase_Auth_Snapshot/src/lib/supabase/proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

function isProtectedPath(pathname: string) {
  return pathname.startsWith('/account') || pathname.startsWith('/tasks')
}

export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet, headers) {
          cookiesToSet.forEach(({ name, value }) =>
reference/TaskFlow_Supabase_Auth_Snapshot/src/lib/supabase/server.ts
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_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet, _headers) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options),
            )
          } catch {