# Les 18 — Lesstof ## Supabase Auth + RLS — multi-user apps in productie **Vak:** AI-Assisted Development **Vorige les:** Les 17 — Externe APIs in diepte **Volgende les:** Geen — eindopdracht thuis --- ## Inhoud 1. [Auth vs Authz](#1-auth-vs-authz) 2. [Supabase Auth — drie methoden](#2-supabase-auth--drie-methoden) 3. [Magic link flow](#3-magic-link-flow) 4. [Social login (OAuth)](#4-social-login-oauth) 5. [Session in Next.js App Router](#5-session-in-nextjs-app-router) 6. [Middleware — sessie refresh](#6-middleware--sessie-refresh) 7. [RLS — wat en waarom](#7-rls--wat-en-waarom) 8. [RLS policies in praktijk](#8-rls-policies-in-praktijk) 9. [Protected routes + server checks](#9-protected-routes--server-checks) 10. [Eindopdracht checklist](#10-eindopdracht-checklist) --- ## 1. Auth vs Authz Twee verschillende vragen — vaak verward. - **Authentication (Auth)** — "wie ben jij?" — login flow - **Authorization (Authz)** — "wat mag jij?" — permissions Voorbeeld: jij bent ingelogd als `tim@example.com` (auth). Dat betekent niet dat je een admin task kan deleten (authz). Authz checkt: heeft deze user de juiste rol/permissie voor deze actie? In Supabase: - **Auth** wordt geregeld door Supabase Auth — magic link, social, password - **Authz** wordt geregeld door RLS — Row Level Security policies in Postgres Geïntegreerd: Supabase Auth zet `auth.uid()` beschikbaar in Postgres, RLS policies gebruiken dat voor filtering. --- ## 2. Supabase Auth — drie methoden ### 2.1 Email + password Klassiek. User registreert met email + wachtwoord. ```typescript const supabase = createClient(...); // Signup await supabase.auth.signUp({ email: "user@example.com", password: "geheim123", }); // Login await supabase.auth.signInWithPassword({ email: "user@example.com", password: "geheim123", }); // Logout await supabase.auth.signOut(); ``` Voordelen: bekend, werkt offline (na login). Nadelen: wachtwoord-management (reset, lekken, weak passwords). ### 2.2 Magic link (passwordless) User vult email in, krijgt link, klikt → ingelogd. ```typescript await supabase.auth.signInWithOtp({ email: "user@example.com", options: { emailRedirectTo: `${location.origin}/auth/callback`, }, }); // Email is verstuurd. User klikt link → redirect naar callback → sessie actief. ``` Voordelen: geen wachtwoord, secure (link bevat one-time token), modern. Nadelen: vereist email-access (geen offline login). **Aanrader voor demo + meeste apps.** ### 2.3 Social login (OAuth) "Login met Google", "Login met GitHub". ```typescript await supabase.auth.signInWithOAuth({ provider: "github", // of "google", "discord", "apple"... options: { redirectTo: `${location.origin}/auth/callback`, }, }); ``` Voordelen: geen account-creation, vertrouwde providers. Nadelen: vereist OAuth-setup bij provider. Supabase ondersteunt 20+ providers: Google, GitHub, Discord, Twitter, Apple, Microsoft, Spotify, etc. --- ## 3. Magic link flow ``` 1. User opent /login 2. Vult email in, klikt Submit 3. Server: supabase.auth.signInWithOtp({ email }) 4. Supabase verstuurt email met magic link: https://your-project.supabase.co/auth/v1/verify?token=...&redirect_to=... 5. User klikt link in email 6. Supabase verifies token, redirect naar je app /auth/callback?code=... 7. Callback route: exchange code voor sessie: await supabase.auth.exchangeCodeForSession(code) 8. Cookie gezet, user is ingelogd ``` ### Callback route in Next.js `app/auth/callback/route.ts`: ```typescript import { createClient } from "@/utils/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}/`); } ``` ### Custom email template Supabase Dashboard → Auth → Email Templates → "Magic Link". Pas onderwerp + body aan. Variabelen: `{{ .Token }}`, `{{ .ConfirmationURL }}`. Belangrijk: voor productie eigen SMTP setup (Resend, Sendgrid). Supabase's default SMTP is rate-limited en sender-reputation onbekend. --- ## 4. Social login (OAuth) ### Setup GitHub 1. GitHub → Settings → Developer settings → OAuth Apps → New OAuth App 2. **Application name:** "Mijn App" 3. **Homepage URL:** `https://yourapp.vercel.app` 4. **Authorization callback URL:** `https://your-project.supabase.co/auth/v1/callback` 5. Save → kopieer Client ID + generate Client Secret ### Configure in Supabase 1. Supabase Dashboard → Auth → Providers → GitHub 2. Enable 3. Plak Client ID + Client Secret 4. Save ### Frontend ```tsx "use client"; import { createClient } from "@/utils/supabase/client"; async function signInWithGitHub() { const supabase = createClient(); await supabase.auth.signInWithOAuth({ provider: "github", options: { redirectTo: `${location.origin}/auth/callback` }, }); } // Render button ``` Klik → redirect naar GitHub → user authorize → terug naar je callback → ingelogd. ### Combineer providers Same app kan magic link **én** GitHub login hebben. User kiest wat hij prefereert. Supabase merged users met dezelfde email automatisch. --- ## 5. Session in Next.js App Router Supabase Auth in Next.js is iets complexer dan andere libraries. Drie redenen: - Cookies moeten gesynced tussen browser + server-side rendering - Sessies verlopen — middleware refreshed automatisch - Server components hebben aparte client nodig dan client components ### Setup ```bash pnpm add @supabase/supabase-js @supabase/ssr ``` ### Vier files **`utils/supabase/client.ts`** — voor Client Components: ```typescript import { createBrowserClient } from "@supabase/ssr"; export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, ); } ``` **`utils/supabase/server.ts`** — voor Server Components / actions: ```typescript 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) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ); } catch {} }, }, }, ); } ``` **`utils/supabase/middleware.ts`** + **`middleware.ts` (root)** — zie volgende sectie. Supabase heeft een **template** voor deze exacte setup. Niet zelf uitvinden, kopieer van docs. --- ## 6. Middleware — sessie refresh Sessies verlopen na een uur (default). Zonder refresh moet user steeds opnieuw inloggen. `utils/supabase/middleware.ts`: ```typescript import { createServerClient } from "@supabase/ssr"; import { NextResponse, type NextRequest } from "next/server"; 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_ANON_KEY!, { cookies: { getAll() { return request.cookies.getAll(); }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)); response = NextResponse.next({ request }); cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options) ); }, }, }, ); await supabase.auth.getUser(); // refreshed sessie return response; } ``` `middleware.ts` (root): ```typescript import { type NextRequest } from "next/server"; import { updateSession } from "@/utils/supabase/middleware"; export async function middleware(request: NextRequest) { return await updateSession(request); } export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], }; ``` Elke request → middleware draait → sessie auto-refreshed → user blijft ingelogd. --- ## 7. RLS — wat en waarom ### Zonder RLS ```typescript // Client met anon key const { data } = await supabase.from("tasks").select("*"); // → Returns ALL tasks from ALL users — disaster ``` Met alleen anon key in browser kan iedereen alles. Niet acceptabel voor multi-user apps. ### Met RLS ```sql alter table tasks enable row level security; create policy "select own" on tasks for select using (auth.uid() = user_id); ``` Now: ```typescript const { data } = await supabase.from("tasks").select("*"); // → Returns alleen tasks waar user_id = auth.uid() ``` Postgres past de filter ALTIJD toe, ongeacht wie de query stelt. Sterke garantie. ### `auth.uid()` Supabase helper functie. Returns: - UUID van ingelogde user (uit JWT) - `NULL` als anonymous Te gebruiken in: - `WHERE` clauses - `default` waarden bij INSERT - Custom functions ### Hoe Postgres weet wie ingelogd is Supabase JS client stuurt JWT mee in `Authorization: Bearer ` header. Postgres parsed JWT, extract user-id, beschikbaar via `auth.uid()`. Magic onder de motorkap, voor jou: werkt gewoon. --- ## 8. RLS policies in praktijk ### Basis: per-user data ```sql create table tasks ( id bigserial primary key, user_id uuid not null references auth.users(id) default auth.uid(), text text not null, done boolean default false ); alter table tasks enable row level security; -- Read create policy "select own tasks" on tasks for select using (auth.uid() = user_id); -- Create create policy "insert own tasks" on tasks for insert with check (auth.uid() = user_id); -- Update create policy "update own tasks" on tasks for update using (auth.uid() = user_id); -- Delete create policy "delete own tasks" on tasks for delete using (auth.uid() = user_id); ``` `using` = check tijdens read (welke rows mag user zien?) `with check` = check tijdens write (welke rows mag user maken?) ### Variant: shared data per team Voor team-features waar users elkaars data zien: ```sql create table team_members ( team_id uuid, user_id uuid, primary key (team_id, user_id) ); create policy "see team tasks" on tasks for select using ( team_id in ( select team_id from team_members where user_id = auth.uid() ) ); ``` ### Variant: admin role Voor admins die alles mogen zien: ```sql create policy "admins see all" on tasks for select using ( auth.jwt() ->> 'role' = 'admin' OR auth.uid() = user_id ); ``` Set custom JWT claims via Supabase Dashboard of code. ### Best practices - **Begin met `enable row level security`** — anders heeft policies geen effect - **Default-deny:** zonder policies = niemand mag iets - **Per-operation policies:** select/insert/update/delete apart - **Test in incognito** — eerlijke check zonder admin-context - **Hou queries simpel** — complexe policies = trage queries --- ## 9. Protected routes + server checks ### Protected page (Server Component) ```tsx import { createClient } from "@/utils/supabase/server"; import { redirect } from "next/navigation"; export default async function Dashboard() { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) redirect("/login"); return
Welkom {user.email}
; } ``` ### Protected layout `app/(authenticated)/layout.tsx`: ```tsx export default async function AuthLayout({ children }: { children: React.ReactNode }) { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) redirect("/login"); return <>{children}; } ``` Alle pages onder `(authenticated)` zijn nu beschermd. ### Protected API route ```typescript import { createClient } from "@/utils/supabase/server"; export async function POST(req: Request) { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return new Response("Unauthorized", { status: 401 }); // user is ingelogd — handle request } ``` ### Logout ```tsx "use client"; import { createClient } from "@/utils/supabase/client"; async function logout() { const supabase = createClient(); await supabase.auth.signOut(); window.location.href = "/login"; } ``` --- ## 10. Eindopdracht checklist Wat je voor je eindopdracht moet hebben: ### Technische stack - ✓ Next.js 16 + TypeScript + Tailwind - ✓ Supabase Postgres - ✓ Vercel AI SDK met Tool Calling - ✓ Minstens 1 externe API - ✓ Multi-user — Supabase Auth + RLS - ✓ Deployed op Vercel - ✓ Code op GitHub met PR's + CI ### Feature must-haves - Login flow (magic link of social) - Per-user data (RLS policies actief) - AI feature met tools (chat, agent, RAG, etc.) - Externe API integratie (geen alleen-OpenAI) - Productie URL die werkt ### Documentatie - README met setup-instructies - ENV vars template (`.env.example`) - Beschrijving van je AI feature(s) - Architectuur-diagram (mag schets zijn) ### Tijd-indicatie 40-60 uur thuiswerk. Verspreid over 4-6 weken. Niet laatste weekend. ### Inleveren - GitHub repo URL - Vercel productie URL - Demo-account credentials (test user) - 2-5 min screen recording van app in actie (optioneel maar aangeraden) --- ## Bronnen - **Supabase Auth:** https://supabase.com/docs/guides/auth - **Next.js setup:** https://supabase.com/docs/guides/auth/server-side/nextjs - **Magic links:** https://supabase.com/docs/guides/auth/auth-magic-link - **Social login:** https://supabase.com/docs/guides/auth/social-login - **RLS guide:** https://supabase.com/docs/guides/database/postgres/row-level-security - **RLS policies:** https://supabase.com/docs/guides/database/postgres/row-level-security#policies - **Custom claims:** https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac - **Auth UI library:** https://supabase.com/docs/guides/auth/auth-helpers/auth-ui