# Les 8 — Docenttekst ## Supabase × Next.js + Auth --- ## Lesoverzicht | Gegeven | Details | |---------|---------| | **Les** | 8 van 18 | | **Onderwerp** | Supabase koppelen + Auth introductie | | **Duur** | 3 uur (09:00 – 12:00) | | **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen, RLS ingesteld | | **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account | --- ## Leerdoelen Na deze les kunnen studenten: 1. De Supabase JavaScript client gebruiken in een Next.js project 2. Data ophalen via Supabase queries (select met relaties, eq, single) 3. Het Server Component + Client Component patroon toepassen 4. Uitleggen wat authenticatie vs autorisatie is 5. Supabase Auth functies gebruiken (signUp, signIn, signOut, getUser) 6. Een login/registratie flow bouwen in Next.js --- ## Lesvoorbereiding (voor docent) Zorg dat je volgende zaken hebt voorbereiding: - Een werkend Supabase project met `polls` en `options` tabellen (uit Les 7) - RLS ingeschakeld op beide tabellen met policies voor SELECT (anon) en UPDATE (anon op options) - De Next.js QuickPoll app uit Les 7 werkend op je machine - De slides gereed voor uitleg authenticatie vs autorisatie - Test je eigen Supabase credentials vooraf --- ## 09:00–09:10 | Welkom & Terugblik (10 min) **Doel:** Studenten krijgen duidelijk wat we vandaag doen en waar we van vorige week waren. ### Wat we hebben gedaan in Les 7: - ✅ Stemmen werkend gemaakt (votePoll functie, state update in poll detail page) - ✅ Supabase introductie: account aangemaakt, project gemaakt - ✅ Database: polls + options tabellen aangemaakt - ✅ Foreign keys + CASCADE ingesteld - ✅ RLS policies ingesteld (SELECT voor anon, UPDATE voor anon op options) - ✅ Testdata ingevoerd via Table Editor ### Wat we NIET hebben afgemaakt in Les 7: - ❌ Supabase is NIET aan het Next.js project gekoppeld - ❌ Data wordt nog niet uit Supabase opgehaald - ❌ Geen authenticatie ### Vandaag gaan we: 1. **DEEL 1 (65 min):** Supabase client installeren en opzetten → data uit database halen in Next.js 2. **DEEL 2a (30 min):** Uitleg over authenticatie, autorisatie en Supabase Auth features 3. **DEEL 2b (30 min):** Studenten bouwen auth zelf in hun project (signup, login, logout) **Motivatie:** "Tot nu toe zijn je polls hardcoded in geheugen. Straks halen we echte data uit Supabase en kunnen people inloggen. Dat is een echt web app!" --- ## 09:10–10:15 | DEEL 1: Supabase Koppelen — Live Coding (65 min) Dit deel volgt een stap-voor-stap aanpak met live coding. Alle studenten coderen mee. ### 09:10–09:15 | Installatie (5 min) Open terminal in het QuickPoll project en run: ```bash npm install @supabase/supabase-js ``` **Teacher Tip:** Controleer dat de installatie slaagt. Als students `npm ERR!` zien, laat ze eerst `npm clean-install` doen en daarna opnieuw proberen. ### 09:15–09:25 | Environment Variables (10 min) Zorg dat alle studenten hun Supabase credentials veilig opslaan. 1. Open in Supabase Dashboard: **Settings** → **API** 2. Kopieer: - `Project URL` (eindigt op `.supabase.co`) - `anon` public key 3. Maak/open `.env.local` in je Next.js project root: ```env NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here ``` **Belangrijk:** - `.env.local` staat al in `.gitignore` (check even) - Keys die beginnen met `NEXT_PUBLIC_` zijn zichtbaar in browser (maar anon keys zijn daarvoor bedoeld) - **ALTIJD de dev server herstarten na wijzigen van `.env.local`** (Ctrl+C, dan `npm run dev`) **Teacher Tip:** Dit is een veelvoorkomende fout. Zeg hardop: "Als jullie een leeg array zien in plaats van polls, check EERST of je dev server herstarten hebt!" ### 09:25–09:35 | Supabase Client aanmaken (10 min) Maak `lib/supabase.ts`: ```typescript import { createClient } from '@supabase/supabase-js' const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! export const supabase = createClient(supabaseUrl, supabaseKey) ``` **Wat gebeurt hier:** - We importeren `createClient` uit `@supabase/supabase-js` - We halen URL en key uit environment variables - We geven deze aan `createClient` - We exporteren de client zodat we het overal kunnen gebruiken **Teacher Tip:** TypeScript geeft mogelijk een warning over "null assertion (!)" — dat is OK. Dit zeggen we tegen TypeScript: "Deze values bestaan echt, vertrouw me." ### 09:35–09:45 | Database Types (10 min) Maak `lib/types.ts` handmatig: ```typescript export interface Poll { id: string created_at: string question: string } export interface Option { id: string poll_id: string text: string votes: number } ``` **Waarom:** Dit helpt TypeScript begrijpen welke data we uit Supabase krijgen. **Teacher Tip:** In een echt project zou je `npx supabase gen types typescript` gebruiken, maar dat kost extra setup. Voor deze les is handmatig OK. ### 09:45–10:00 | Async Data functies (15 min) Update `lib/data.ts` — alle functies worden nu async en halen data uit Supabase: ```typescript import { supabase } from './supabase' import { Poll, Option } from './types' export async function getPolls(): Promise { const { data, error } = await supabase .from('polls') .select('*') .order('created_at', { ascending: false }) if (error) { console.error('Error fetching polls:', error) return [] } return data || [] } export async function getOptions(pollId: string): Promise { const { data, error } = await supabase .from('options') .select('*') .eq('poll_id', pollId) .order('votes', { ascending: false }) if (error) { console.error('Error fetching options:', error) return [] } return data || [] } ``` **Wat betekent dit:** - `.from('polls')` — welke tabel - `.select('*')` — alle kolommen - `.eq('poll_id', pollId)` — filter op poll_id - `.order()` — sorteer op - `await` — wacht op het resultaat van de database call - Error handling — log en return empty array **Teacher Tip:** Veel students maken hier fouten met async/await: ```typescript // ❌ FOUT: promise niet awaited! const data = supabase.from('polls').select('*') // ✅ GOED: const data = await supabase.from('polls').select('*') ``` ### 10:00–10:10 | Homepage als Server Component (10 min) Update `app/page.tsx` — dit wordt een Server Component: ```typescript import { getPolls } from '@/lib/data' import PollItem from '@/components/PollItem' export default async function Home() { const polls = await getPolls() return (

QuickPoll

{polls.map((poll) => ( ))}
{polls.length === 0 && (

Geen polls beschikbaar.

)}
) } ``` **Belangrijk:** Page.tsx is nu een **Server Component** — geen `'use client'` directive! We kunnen hier `async/await` rechtstreeks gebruiken. **Teacher Tip:** Students vragen: "Maar hoe krijgen we de options?" — Goed punt! Die halen we in PollItem. ### 10:10–10:15 | PollItem Component (5 min) Update `components/PollItem.tsx` — ook een Server Component: ```typescript import { getOptions } from '@/lib/data' import VoteForm from './VoteForm' import { Poll } from '@/lib/types' export default async function PollItem({ poll }: { poll: Poll }) { const options = await getOptions(poll.id) return (

{poll.question}

{options.map((option) => ( ))}
) } ``` **Waarom twee Server Components?** - `page.tsx` ziet alleen alle polls (geen details) - `PollItem` wordt per poll gerenderd en haalt zelf de options op (parallel!) - Dit patroon is efficient en schaalbaar **Teacher Tip:** Dit is het "Suspended Components" patroon van React 18 — Server Components voeren dit automatisch in parallel uit. --- ## 10:15–10:30 | PAUZE (15 min) Goed moment om even weg te lopen. Tussendoor kun jij: - Rondlopen en kijken wie nog errors heeft - Checken of iedereen env vars juist ingesteld heeft - Dev servers herstarten voor wie vergeten zijn - Voorbereiding treffen voor DEEL 2 --- ## 10:30–11:00 | DEEL 2a: Uitleg Auth (30 min) Dit is uitleg — geen live coding nog. Zorg dat alle laptops dicht zijn, focus op slides en beamer. ### Authenticatie vs Autorisatie **Authenticatie (Authentication):** - "Wie ben je?" — identity verification - Voorbeeld: Je logt in met email + password - Supabase Auth zorgt hiervoor **Autorisatie (Authorization):** - "Wat mag je?" — permissions - Voorbeeld: Je mag alleen je eigen polls aanpassen - RLS (Row Level Security) in Supabase zorgt hiervoor **Voorbeeld:** - Auth: "Je email en password kloppen, je bent Alice." - RLS: "Alice mag haar eigen polls zien en updaten, maar niet die van Bob." ### Supabase Auth Features Demo op beamer: 1. Open Supabase Dashboard → **Authentication** → **Providers** 2. Toon dat **Email/Password** is ingeschakeld 3. Toon de instelling **"Confirm email"** (nu UIT voor dev) 4. Ga naar **Users** tab — hier zie je ingelogde users **Supabase Auth ondersteunt:** - Email/Password (wat we vandaag gebruiken) - OAuth (Google, GitHub, etc.) — volgende week - Magic Links (passwordless login) - Session management (Supabase beheert cookies automatisch) ### @supabase/ssr vs @supabase/supabase-js **@supabase/supabase-js:** - Browser-side client - Voor onClick handlers, forms, interactie **@supabase/ssr:** - Server-side client (SSR = Server-Side Rendering) - Voor middleware, cookies, server actions - Handelt sessions automatisch af **Waarom twee?** - Browser kan niet veilig geheimen beheren - Server kan veilig cookies zetten - Supabase SSR packages zorgen dat beide veilig werken ### Supabase Auth Functies **signUp(email, password)** — nieuwe account aanmaken ```typescript const { data, error } = await supabase.auth.signUp({ email: 'user@example.com', password: 'secure-password' }) ``` **signInWithPassword(email, password)** — inloggen ```typescript const { data, error } = await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'secure-password' }) ``` **signOut()** — uitloggen ```typescript await supabase.auth.signOut() ``` **getUser()** — huidge user ophalen ```typescript const { data: { user } } = await supabase.auth.getUser() // user is null als niemand ingelogd, anders is het een User object ``` ### Server vs Browser Client **Browser Client (createBrowserClient):** - Voor 'use client' components - Kan useState gebruiken - Kan useRouter gebruiken - Kan user events luisteren **Server Client (createServerClient):** - Voor server components en middleware - Leest/schrijft cookies - Kan getUser() veilig aanroepen - Geen access tot browser APIs ### Middleware & Session Refresh **Wat doet middleware?** - Draait op elke request naar je app - Refreshed de Supabase session - Zorgt dat user state altijd up-to-date is **Voorbeeld flow:** 1. User logt in op `/login` page 2. Cookie wordt gezet 3. Middleware ziet op volgende request: "Er is een session cookie!" 4. Middleware refreshed de session 5. App ziet dat user ingelogd is ### Handige links Toon op slides: - [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs) - [Next.js Server Components docs](https://nextjs.org/docs/getting-started/react-essentials) --- ## 11:00–11:30 | DEEL 2b: Zelf Doen — Auth Implementeren (30 min) Nu gaan studenten zelf auth bouwen in hun project. Dit is niet meer live coding — docent loopt rond en helpt. **Instructie voor studenten:** Volg deze stappen. Docent loopt rond als je vragen hebt. #### Stap 1: SSR Package Installeren (2 min) ```bash npm install @supabase/ssr ``` #### Stap 2: Server Client (3 min) Maak `lib/supabase-server.ts`: ```typescript import { cookies } from 'next/headers' import { createServerClient } from '@supabase/ssr' 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 { // Handle error } }, }, } ) } ``` **Wat is dit?** Dit is een helper zodat Supabase cookies kan beheren in Next.js. Copy-paste voor nu. #### Stap 3: Browser Client (1 min) Maak `lib/supabase-browser.ts`: ```typescript import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) } ``` **Wat is dit?** Dit gebruiken we in 'use client' components. #### Stap 4: Middleware (5 min) Maak `middleware.ts` in project root: ```typescript import { type NextRequest, NextResponse } from 'next/server' import { createServerClient } from '@supabase/ssr' 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) }) }, }, } ) // Refresh user session await supabase.auth.getUser() return supabaseResponse } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.*\\.svg|.*\\.png|.*\\.jpg|.*\\.jpeg).*)', ], } ``` **Wat is dit?** Dit draait op elke request en refreshed de session. Copy-paste, don't worry. #### Stap 5: Signup Page (5 min) Maak `app/auth/signup/page.tsx`: ```typescript 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { createClient } from '@/lib/supabase-browser' export default function SignUpPage() { const router = useRouter() const supabase = createClient() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const handleSignUp = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError('') const { error } = await supabase.auth.signUp({ email, password, }) if (error) { setError(error.message) setLoading(false) } else { router.push('/auth/login') } } return (

Sign Up

{error &&
{error}
} setEmail(e.target.value)} className="w-full px-4 py-2 border rounded mb-4" required /> setPassword(e.target.value)} className="w-full px-4 py-2 border rounded mb-6" required />
) } ``` **Belangrijk:** `'use client'` directive bovenaan — dit is een interactive component! #### Stap 6: Login Page (5 min) Maak `app/auth/login/page.tsx`: ```typescript 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { createClient } from '@/lib/supabase-browser' import Link from 'next/link' export default function LoginPage() { const router = useRouter() const supabase = createClient() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError('') const { error } = await supabase.auth.signInWithPassword({ email, password, }) if (error) { setError(error.message) setLoading(false) } else { router.push('/') } } return (

Login

{error &&
{error}
} setEmail(e.target.value)} className="w-full px-4 py-2 border rounded mb-4" required /> setPassword(e.target.value)} className="w-full px-4 py-2 border rounded mb-6" required />

Nog geen account? Sign up

) } ``` #### Stap 7: Logout Button (3 min) Maak `components/LogoutButton.tsx`: ```typescript 'use client' import { useRouter } from 'next/navigation' import { createClient } from '@/lib/supabase-browser' export default function LogoutButton() { const router = useRouter() const supabase = createClient() const handleLogout = async () => { await supabase.auth.signOut() router.refresh() } return ( ) } ``` **Belangrijk:** `router.refresh()` na logout zorgt dat page de nieuwe state ziet! #### Stap 8: Navbar met Auth State (3 min) Update `components/Navbar.tsx`: ```typescript import { createClient } from '@/lib/supabase-server' import Link from 'next/link' import LogoutButton from './LogoutButton' export default async function Navbar() { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() return ( ) } ``` **Logica:** - Als `user` bestaat (ingelogd): toon email + Logout button - Anders: toon Login + Sign Up buttons #### Stap 9: Layout updaten (2 min) Update `app/layout.tsx`: ```typescript import type { Metadata } from 'next' import Navbar from '@/components/Navbar' import './globals.css' export const metadata: Metadata = { title: 'QuickPoll', description: 'Vote on polls', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ``` Voeg gewoon `` toe. **Teacher Tip: Studenten vastlopen?** - Na 5-10 minuten vastzitten: toon de referentie code op beamer - Zeg: "Dit is complex, copy-paste is OK. Focus op begrijpen, niet op typen." - Help met debuggen (console.log, errors lezen) --- ## 11:30–11:45 | Vragen & Reflectie (15 min) Hier zijn veelvoorkomende vragen: ### V: "Wat is het verschil tussen `createClient()` in server.ts en browser.ts?" **A:** - `server.ts`: kan cookies veilig beheren (server-side) - `browser.ts`: kan UI events afhandelen (onClick, forms) - Supabase kiest automatisch het juiste moment om te gebruiken ### V: "Waarom twee environment variables bovenaan?" **A:** - `NEXT_PUBLIC_SUPABASE_URL`: URL is public, iedereen ziet het - `NEXT_PUBLIC_SUPABASE_ANON_KEY`: anon key is public (maar kan geen private data lezen) - Private keys (service role) zetten we NIET in .env.local, die gaan in server.ts als geheim ### V: "Mijn login werkt niet, ik krijg error" **A:** Check: 1. Klopt je email/password echt? 2. Is je account in Supabase Dashboard → Authentication → Users? 3. Is Email provider ingeschakeld? 4. Zit "Confirm email" uit? (check dashboard) ### V: "Logout werkt niet, user staat nog ingelogd" **A:** Vergeten `router.refresh()` na `signOut()`? ### V: "Middleware error: 'createServerClient is not defined'" **A:** Check je import: moet `import { createServerClient } from '@supabase/ssr'` zijn ### V: "Kan ik als anonieme user stemmen?" **A:** Ja! RLS policy staat op `FOR SELECT, UPDATE TO authenticated` — maar je Navbar toont Login/Signup want je bent nog niet ingelogd. Dat is OK. Volgende les doen we RLS policies correct. --- ## 11:45–12:00 | Huiswerk & Afsluiting (15 min) ### Huiswerk (voor Les 9): **Verplicht:** 1. **/create pagina bouwen** — studenten voegen nieuwe polls toe via een form - Maak `app/create/page.tsx` (Server Component met form als Client Component) - Form met: vraag + array van 2-3 opties - `supabase.from('polls').insert()` en `supabase.from('options').insert()` - Zorg dat je eigen `user_id` meestuurt 2. **RLS INSERT policy** — alleen authenticated users mogen polls toevoegen - Supabase Dashboard → Authentication → Policies - Voeg policy toe: `INSERT` voor authenticated users - `user_id = auth.uid()` 3. **Optional extras (challenge):** - Toon poll creator in PollItem - Google OAuth inschakelen (zie Supabase docs) - Edit/Delete buttons (alleen voor je eigen polls) ### Afsluitingsboodschap: "Gefeliciteerd! Vandaag hebben jullie: - Supabase gekoppeld aan Next.js - Real data uit een database geladen - Login/logout gebouwd - Server & browser clients begrepen Volgende week voegen we RLS policies toe zodat iedereen alleen zijn eigen polls kan aanpassen. Dat is waar authenticatie écht nuttig wordt!" --- ## Veelvoorkomende Problemen | Probleem | Oorzaak | Oplossing | |----------|---------|-----------| | `Error: Cannot find module '@supabase/supabase-js'` | Package niet geïnstalleerd | `npm install @supabase/supabase-js` en dev server herstarten | | Supabase returns leeg array | .env.local niet juist of dev server niet herstarten | Check .env.local, restart dev server (Ctrl+C + `npm run dev`) | | TypeScript complains over `null assertion (!)` | Normale TS warning | Dit is OK, we vertellen TS dat env vars bestaan | | `'use client' vergeten in signup/login page` | Component is interactief maar geen directive | Voeg `'use client'` bovenaan toe | | Login page blank/geen content | Conflict met server components | Zorg ALL pages onder /auth zijn `'use client'` | | Logout werkt niet, user nog ingelogd | `router.refresh()` niet aangeroepen | Voeg `await router.refresh()` toe na `signOut()` | | Middleware error: "wrong params" | Onjuiste URL of key in middleware | Copy-paste van .env.local, check Format | | "Invalid token" bij Supabase calls | Token verlopen of anon key fout | Restart dev server, check API credentials | | User niet in Authentication → Users | Signup failed, geen account aangemaakt | Check browser console op errors, probeer opnieuw met ander email | | `router.refresh()` werkt niet in component | Router niet geïmporteerd | `import { useRouter } from 'next/navigation'` (niet 'next/router'!) | | Cors/network error | Supabase URL fout | Check dat URL eindigt op `.supabase.co` en https:// bevat | | Password te kort / validation error | Supabase vereist min 6 chars | Zeg studenten: "Test met password123" | --- ## Didactische Tips - **Pair Programming:** Zet snelle studenten samen met tragere — kennis spreidt zich uit - **Show & Tell:** Toon je eigen werkend QuickPoll op beamer — studenten zien het doel - **Error-driven Learning:** Zeg niet meteen het antwoord, vraag: "Wat zegt de error?" - **Debug together:** Als iemand vastlopen, use browser console.log + devtools - **Save time** — als >3 students dezelfde error hebben, stop even en toon op beamer - **Celebrate wins** — als iemand eerste Signup working heeft, geef thumbs up! --- ## Referentiematerialen voor Studenten - [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs) - [Next.js Server Components](https://nextjs.org/docs/getting-started/react-essentials) - [Environment Variables in Next.js](https://nextjs.org/docs/basic-features/environment-variables) - Alle code snippets uit deze docenttekst --- **Einde docenttekst Les 8**