Let Supabase Auth establish user identity while the application still owns TaskFlow authorization rules.
Course navigationPhase 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.
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.
Principles to keep while coding
Keep service-role credentials off the browser and use public client credentials only for intended client operations.
Handle session restoration and expiration explicitly so UI state does not silently drift from server identity.
Verify authentication on the server for protected operations rather than trusting client-visible session state.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Configure project URLs/redirects and explain why local/preview/production values differ.
- 02
Create browser and server Supabase clients with the correct key exposure model.
- 03
Implement sign-up/sign-in and surface provider errors safely.
- 04
Render authenticated state on the server rather than trusting only client state.
- 05
Implement sign-out and prove protected routes recover to unauthenticated state.
- 06
Inspect the session/access token and prepare the boundary for NestJS verification without exposing service-role credentials.
Failures you should know how to diagnose
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.
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.
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.
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.
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.
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
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
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
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
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
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
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
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
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
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
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
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.
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.
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?
Mastery Capstone Lab
Integrate Phase 17 without following a transcript.
Exercises that increase independence
Configure project URLs/redirects and explain why local/preview/production values differ.
Implement sign-up/sign-in and surface provider errors safely.
reproduce and diagnose — Using service-role key in browser code.
deliver Working TaskFlow authentication UI/session integration whose server-side checks are explicit and ready to hand tokens to NestJS.
Can distinguish public and secret Supabase keys and Can create browser/server clients.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
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>
Emailreference/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 {