Files
novi-lessons/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md
2026-03-31 16:06:18 +02:00

2473 lines
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Les 8 — Live Coding Guide
## Supabase × Next.js + Auth
> **Jouw spiekbriefje.** Dit bestand staat op je privéscherm. Op de beamer draait Cursor.
> Volg stap voor stap. Typ exact wat hier staat. Leg uit met de "Vertel:" blokken.
---
## VOOR DE LES BEGINT
**Zorg dat je dit hebt klaargemaakt:**
- [ ] Het poll-demo project geopend in Cursor
- [ ] feature/les-8 branch checked out (VOOR de nieuwe commits)
- [ ] De code hieronder klopt met wat je ziet (BEFORE-state)
- [ ] Supabase project aangemaakt en database klaar (polls + options tabellen)
- [ ] Twee demo polls in de database
- [ ] Slides geopend (voor Deel 2)
---
# DEEL 1: LIVE CODING — Supabase Koppelen (09:1010:15)
**Doel:** De poll-app offline maken en live verbinden met Supabase. Alle data gaat via het Supabase API.
---
## STAP 1: Installeer @supabase/supabase-js
```bash
npm install @supabase/supabase-js
```
**Vertel:**
"We gaan de Supabase client library installeren. Dit is de JavaScript SDK waarmee we rechtstreeks kunnen communiceren met Supabase — zonder een eigen backend API te hoeven schrijven."
---
## STAP 2: Maak .env.local aan
Bestand: `.env.local` (in de root van je project)
```
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**Vertel:**
"Deze environment variables bevatten je Supabase project URL en de anonieme API key. Ze beginnen beide met NEXT_PUBLIC_, dus ze worden in de browser zichtbaar — maar dat is oké want dit zijn public credentials. Je private key bewaar je nooit in de client code."
**Waar vind je deze?**
- Ga naar je Supabase project dashboard
- Settings → API
- Kopieer Project URL en anon key
---
## STAP 3: Maak lib/supabase.ts aan
Bestand: `lib/supabase.ts`
```typescript
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
```
**Vertel:**
"Dit is onze Supabase client — één plek waar we de verbinding opzetten. We gebruiken `createClient()` om één client aan te maken die we overal importeren. De `!` betekent 'deze mag nooit undefined zijn'."
---
## STAP 4: Update types/index.ts
**BEFORE:**
```typescript
export interface Poll {
id: string;
question: string;
options: string[];
votes: number[];
}
```
**AFTER:**
```typescript
export interface Poll {
id: string;
question: string;
created_at: string;
options: Option[];
}
export interface Option {
id: string;
poll_id: string;
text: string;
votes: number;
}
```
**Vertel:**
"De database structuur is anders dan wat we hadden. Nu hebben we twee aparte tabellen: `polls` en `options`. Elke option is een eigen record met een `poll_id` referentie. Dit is genormaliseerde data — beter voor grote apps. De votes tellen we per option, niet meer als een array."
---
## STAP 5: Rewrite lib/data.ts
**BEFORE:**
```typescript
import { Poll } from "@/types";
let polls: Poll[] = [
{ id: "1", question: "Ik ben een vraag", options: ["optie 1", "optie 2", "optie 3", "optie 4"], votes: [1,1,1,1] },
{ id: "2", question: "Ik ben een vraag 2", options: ["optie 1", "optie 2", "optie 3", "optie 4"], votes: [1,1,1,1] }
]
export function getPolls(): Poll[] { return polls }
export function getPollById(id: string): Poll | undefined { return polls.find((poll) => poll.id === id) }
export function votePoll(id: string, optionIndex: number) {
const poll = polls.find((p) => p.id === id)
if (!poll) return undefined;
if (optionIndex < 0 || optionIndex >= poll.options.length) return undefined;
poll.votes[optionIndex]++;
return poll;
}
```
**AFTER:**
```typescript
import { supabase } from "./supabase";
import { Poll, Option } from "@/types";
export async function getPolls(): Promise<Poll[]> {
const { data: polls, error } = await supabase
.from("polls")
.select("*, options(*)")
.order("created_at", { ascending: false });
if (error) {
console.error("Error fetching polls:", error);
return [];
}
return polls || [];
}
export async function getPollById(id: string): Promise<Poll | null> {
const { data: poll, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) {
console.error("Error fetching poll:", error);
return null;
}
return poll;
}
export async function votePoll(pollId: string, optionId: string): Promise<Option | null> {
const { data: option, error: fetchError } = await supabase
.from("options")
.select("votes")
.eq("id", optionId)
.single();
if (fetchError || !option) {
console.error("Error fetching option:", fetchError);
return null;
}
const { data: updated, error: updateError } = await supabase
.from("options")
.update({ votes: option.votes + 1 })
.eq("id", optionId)
.select()
.single();
if (updateError) {
console.error("Error updating votes:", updateError);
return null;
}
return updated;
}
```
**Vertel:**
"Nu zijn al onze functies `async` omdat we naar de database praten. Geen in-memory array meer.
- `getPolls()`: haalt alle polls op, sorteert ze op nieuwste eerst, en haalt de options mee met `options(*)`.
- `getPollById()`: haalt één poll op met `.single()` — dat gaat een error geven als de poll niet gevonden wordt.
- `votePoll()`: haalt eerst het huidige votes getal op, plus 1, en schrijft het terug naar de database.
Alle responses volgen het Supabase patroon: `{ data, error }`."
---
## STAP 6: Update app/page.tsx
**BEFORE:**
```typescript
'use client'
import { getPolls } from "@/lib/data";
import { PollItem } from "@/components/PollItem";
export default function Home() {
const polls = getPolls()
const onClick = () => {
console.log('klik')
}
return (
<div className="w-full p-4">
<h2>Onze polls!</h2>
{
polls.map((poll) => {
return <PollItem poll={poll} onOptionClick={onClick} />
})
}
</div>
);
}
```
**AFTER:**
```typescript
import { getPolls } from "@/lib/data";
import { PollItem } from "@/components/PollItem";
import Link from "next/link";
export default async function Home() {
const polls = await getPolls();
return (
<div className="w-full p-4">
<h2 className="text-2xl font-bold mb-4">Onze polls</h2>
{polls.map((poll) => (
<Link key={poll.id} href={`/poll/${poll.id}`}>
<PollItem poll={poll} />
</Link>
))}
</div>
);
}
```
**Vertel:**
"Dit is een gigantische verandering. We VERWIJDEREN `'use client'` — deze component wordt een Server Component. Waarom?
1. `getPolls()` is async — kan alleen op de server runnen.
2. We hoeven geen onClick handler — de PollItem is nu read-only, geen interactie.
3. We wrappen elke poll in een `<Link>` zodat je kan doorklikken naar de detail pagina.
Op de server kunnen we veilig naar Supabase praten zonder de API key in de browser bloot te leggen."
---
## STAP 7: Update components/PollItem.tsx
**BEFORE:**
```typescript
'use client'
import { Poll } from "@/types"
type PollItemProps = { poll: Poll, onOptionClick?: (option: string, index: number) => void }
type PollItemOptionProps = { option: string; onOptionClick?: (option: string, index: number) => void; index: number }
export const PollItemOption = ({option, onOptionClick, index}: PollItemOptionProps) => {
const onClick = () => { if (onOptionClick) onOptionClick(option, index) }
return <button onClick={onClick}>{option}</button>
}
export const PollItem = ({poll, onOptionClick}: PollItemProps) => {
const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0);
return <section className="w-full my-10">
<h2 className="text-xl font-bold">{poll.question} - {totalVotes} votes</h2>
{poll.options.map((option, index) => {
return <PollItemOption key={option} option={option} index={index} onOptionClick={onOptionClick} />
})}
</section>
}
```
**AFTER:**
```typescript
'use client'
import { Poll, Option } from "@/types"
type PollItemProps = {
poll: Poll
onOptionClick?: (option: Option) => void
}
type PollItemOptionProps = {
option: Option
percentage: number
onClick?: (option: Option) => void
}
export const PollItemOption = ({ option, percentage, onClick }: PollItemOptionProps) => {
return (
<div
onClick={() => onClick?.(option)}
className="relative my-2 p-3 border rounded cursor-pointer hover:bg-gray-50 overflow-hidden"
>
<div
className="absolute top-0 left-0 h-full bg-blue-100 transition-all duration-300"
style={{ width: `${percentage}%` }}
/>
<div className="relative flex justify-between">
<span>{option.text}</span>
<span className="text-gray-500">{option.votes} ({percentage}%)</span>
</div>
</div>
)
}
export const PollItem = ({ poll, onOptionClick }: PollItemProps) => {
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
return (
<section className="w-full my-6">
<h2 className="text-xl font-bold mb-2">{poll.question}</h2>
<p className="text-sm text-gray-500 mb-3">{totalVotes} stemmen</p>
{poll.options.map((option) => {
const percentage = totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0;
return (
<PollItemOption
key={option.id}
option={option}
percentage={percentage}
onClick={onOptionClick}
/>
)
})}
</section>
)
}
```
**Vertel:**
"De nieuwe PollItemOption heeft twee grote veranderingen:
1. **Visueel:** We gebruiken een overlay div die wordt ingesteld op `${percentage}%` breedte. Dit geeft de voortgangsbalk effect.
2. **Data:** De option is nu een object `{ id, text, votes, poll_id }` — niet meer een string. We tonen ook de percentage.
3. **Interactie:** `onClick` geeft nu de hele Option door, niet een index. Dat is handiger voor het API call dat we straks doen."
---
## STAP 8: Maak components/VoteForm.tsx aan
Bestand: `components/VoteForm.tsx`
```typescript
'use client'
import { Poll, Option } from "@/types";
import { PollItem } from "./PollItem";
import { useState } from "react";
export function VoteForm({ poll: initialPoll }: { poll: Poll }) {
const [poll, setPoll] = useState(initialPoll);
const onVote = async (option: Option) => {
const response = await fetch(`/api/polls/${poll.id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ optionId: option.id }),
});
if (response.ok) {
const updatedPoll = await response.json();
setPoll(updatedPoll);
}
};
return <PollItem poll={poll} onOptionClick={onVote} />;
}
```
**Vertel:**
"Dit is een nieuw onderdeel: een Client Component die zorgt voor interactie. Op de server kunnen we geen `useState` gebruiken. Dus we nemen de poll van de server, steken die in `useState`, en als de gebruiker stemt:
1. Sturen we een POST naar `/api/polls/{id}` met de optionId.
2. De server verwerkt de stem in Supabase.
3. De server stuurt de updated poll terug.
4. We updaten `setPoll()` met de nieuwe data.
5. De UI re-render met de nieuwe votes.
Dit patroon heet 'Server Component met Client Component wrapper'."
---
## STAP 9: Update app/poll/[id]/page.tsx
**BEFORE:**
```typescript
'use client'
import { useEffect, useState } from "react";
import { PollItem } from "@/components/PollItem";
import { getPollById, votePoll } from "@/lib/data";
import { useParams } from "next/navigation";
export default function Poll() {
const params = useParams()
const pollId = params.id as string
const [poll, setPoll] = useState(() => getPollById(pollId))
const onPollItemClick = async (option: string, index: number) => {
const updatedPoll = votePoll(pollId, index)
if (!updatedPoll) return
setPoll({
...updatedPoll,
votes: [...updatedPoll.votes],
})
}
return (
<div className="w-full p-4">
<h2>Poll</h2>
{poll ? <PollItem poll={poll} onOptionClick={onPollItemClick} /> : <div>no poll</div>}
</div>
);
}
```
**AFTER:**
```typescript
import { getPollById } from "@/lib/data";
import { VoteForm } from "@/components/VoteForm";
import { notFound } from "next/navigation";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PollPage({ params }: PageProps) {
const { id } = await params;
const poll = await getPollById(id);
if (!poll) notFound();
return (
<div className="w-full p-4">
<h2 className="text-2xl font-bold mb-4">{poll.question}</h2>
<VoteForm poll={poll} />
</div>
);
}
```
**Vertel:**
"We verwijderen `'use client'`, `useParams()`, `useState()` — alles. Deze pagina wordt pure Server Component.
- We halen de poll op met `await getPollById(id)`.
- Als die niet bestaat, geven we `notFound()` terug — dat triggert Next.js' 404 pagina.
- Anders, tonen we de poll en geven die door aan `<VoteForm />` (die client component die we net maakten).
Merk op: `params` is nu een Promise. Je moet eerst `await params` doen. Dat is nieuw in Next.js 15+."
---
## STAP 10: Maak app/api/polls/[id]/route.ts aan
**BEFORE (alleen POST, alleen console.log):**
```typescript
import { NextResponse } from "next/server"
interface RouteParams { params: Promise<{id: string}> }
export async function POST(request: Request, {params}: RouteParams) {
const p = await params
console.log('hello from server', p)
return NextResponse.json({success: true})
}
```
**AFTER:**
```typescript
import { NextResponse } from "next/server";
import { getPollById, votePoll } from "@/lib/data";
interface RouteParams {
params: Promise<{ id: string }>;
}
export async function GET(request: Request, { params }: RouteParams) {
const { id } = await params;
const poll = await getPollById(id);
if (!poll) {
return NextResponse.json({ error: "Poll niet gevonden" }, { status: 404 });
}
return NextResponse.json(poll);
}
export async function POST(request: Request, { params }: RouteParams) {
const { id } = await params;
const body = await request.json();
const { optionId } = body;
if (!optionId) {
return NextResponse.json({ error: "optionId is verplicht" }, { status: 400 });
}
const updatedOption = await votePoll(id, optionId);
if (!updatedOption) {
return NextResponse.json(
{ error: "Kon stem niet verwerken" },
{ status: 400 }
);
}
const poll = await getPollById(id);
return NextResponse.json(poll);
}
```
**Vertel:**
"Dit is ons API endpoint. Twee functies:
**GET:**
- Haalt één poll op.
- Stuurt JSON terug: `{ id, question, options: [...] }`
**POST:**
- Body verwacht: `{ optionId: '...' }`
- Roept `votePoll()` aan om de stem te verwerken.
- Haalt daarna de hele updated poll op.
- Stuurt die terug, zodat VoteForm.tsx kan updaten.
Dit is het 'bridge' tussen de client (VoteForm) en de database (Supabase)."
---
## STAP 11: Test — Stem, Refresh, Check Supabase
**Live testen:**
1. **Start de app:**
```bash
npm run dev
```
2. **Homepage testen:**
- Ga naar http://localhost:3000
- Je ziet twee polls (uit de database)
- Klik op één poll
3. **Stem testen:**
- Je ziet de detail pagina
- Klik op een optie
- De votes verhogen
- Refresh de pagina — de votes blijven!
4. **Check Supabase:**
- Ga naar je Supabase dashboard
- Table Editor → `options`
- Zie je dat de votes zijn gestegen? Perfect!
**Vertel:**
"Dit is het moment van waarheid. Je hebt:
- Een Server Component dat de poll ophaalt (homepage).
- Een Server Component dat de detail pagina rendert.
- Een Client Component die stemmen verwerkt.
- Een API route die database queries doet.
Alles samengebracht. De data leeft nu in Supabase, niet in het geheugen. Elke refresh, andere browser — je ziet dezelfde cijfers. Dat is realtime data."
---
---
# DEEL 2: UITLEG + ZELF DOEN — Supabase Auth (10:3011:30)
**Doel:** Studenten leren hoe Supabase Auth werkt en implementeren het zelf.
**Nota bene:** Dit is GEEN live typing. Je laat slides en documentatie zien. Studenten implementeren zelf.
---
## FASE 1: UITLEG (30 minuten)
**Structuur:**
- 10 min: Concepten (slides)
- 10 min: Packages & patterns (documentatie)
- 10 min: Code snippets (scherm)
- Vragen beantwoorden
---
### Slide: Authenticatie vs Autorisatie
**Vertel:**
"Twee woorden die veel mensen verwarren.
**Authenticatie** = 'Wie ben je?' — Je inloggen met email en wachtwoord. De server checkt: bestaat deze gebruiker, en is het wachtwoord correct?
**Autorisatie** = 'Mag je dit doen?' — Na inloggen. Je bent ingelogd, maar mag je deze pagina zien? Deze poll aanpassen? Een ander artikel verwijderen?
Vandaag doen we vooral authenticatie. Autorisatie (RLS — Row Level Security) doen we snel aan het einde.
Supabase Auth handelt authenticatie af. SQL-policies handelen autorisatie af."
---
### Slide: Wat Supabase Auth Biedt
**Vertel:**
"Supabase Auth ondersteunt:
1. **Email + Password:** standaard. Je geeft email + wachtwoord in, Supabase slaat het op (gehashed), je krijgt een session token terug.
2. **OAuth:** Inloggen met Google, GitHub, Discord, etc. Geen wachtwoord nodig.
3. **Magic Links:** Je voert email in, Supabase stuurt een link. Je klikt, je bent ingelogd. Geen wachtwoord.
4. **Passwordless:** Andere methodes.
Vandaag doen we Email + Password. Simpel, goed te testen."
---
### Demo: Supabase Dashboard — Email Providers
**Zet live op scherm:**
1. Ga naar je Supabase dashboard
2. Authentication → Providers
3. Email → klik erop
4. Je ziet een toggle: "Confirm email"
**Vertel:**
"In production zet je dit AAN. Dan moet de gebruiker zijn email verifiëren voor hij inloggen kan.
Voor development zet je het UIT. Dan kan je direct inloggen zonder email te verifiëren. Veel sneller testen."
**Zet dit UIT voor de les.**
---
### Slide: De Packages
**Vertel:**
"We gebruiken twee libraries:
1. **`@supabase/supabase-js`** — Wat je al hebt. Dit werkt prima in de browser, maar cookies worden niet goed gehandhaafd.
2. **`@supabase/ssr`** — SSR = Server-Side Rendering. Dit bevat `createServerClient` en `createBrowserClient`. Die managen cookies netjes, zodat je session op alle requests geldig blijft.
Waarom? In Next.js App Router renderen je pagina's op de server. Je moet veilig cookies kunnen lezen en schrijven. Dit pakket doet dat goed."
**Laat de docs zien:**
```
https://supabase.com/docs/guides/auth/server-side/nextjs
```
**Vertel:**
"Dit is je Bijbel. Hier staat alles. Wij gaan samen dit patroon implementeren."
---
### Slide: Server Client vs Browser Client
**Code snippets op scherm (niet live typen):**
**Server Client:**
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
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)
);
},
},
}
);
}
```
**Browser Client:**
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
**Vertel:**
"Twee clients:
**Server Client:**
- Runt op de server (Next.js pages, API routes).
- Heeft toegang tot `cookies()` van Next.js.
- Kan de session bijhouden via cookies.
- Geen user interaction nodig — alles asynchroon.
**Browser Client:**
- Runt in de browser (Client Components).
- Veel simpeler — geen cookies nodig, localStorage is genoeg.
- Gebruikt voor interactieve onderdelen.
Maak twee bestanden: `lib/supabase-server.ts` en `lib/supabase-browser.ts`. Daarin zet je deze functies. Dan importeer je de juiste in elk bestand."
---
### Slide: Middleware — Session Refresh
**Code snippet op scherm (niet live typen):**
```typescript
// middleware.ts (in root folder)
import { NextResponse, type NextRequest } from "next/server";
import { createServerClient } from "@supabase/ssr";
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
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 }) =>
response.cookies.set(name, value, options)
);
},
},
}
);
// Refresh the user's session (if it exists)
await supabase.auth.getUser();
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
```
**Vertel:**
"Middleware draait op ELKE request naar je app. Het enige dat het doet:
1. Maak een Supabase server client.
2. Roep `getUser()` aan — dit refresht de session als die bijna verlopen is.
3. Geef de response terug met de bijgewerkte cookies.
Dit zorgt dat je gebruiker ingelogd blijft, zelfs na 1 uur inactiviteit. Supabase verlengt de session automatisch."
---
### Slide: De Vier Basis Functions
**Vertel:**
"Je hebt vier functies nodig om alles te doen. Schrijf ze in je head mee:"
**1. Registreren:**
```typescript
const { error } = await supabase.auth.signUp({
email: "user@example.com",
password: "password123"
});
```
**2. Inloggen:**
```typescript
const { error } = await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "password123"
});
```
**3. Uitloggen:**
```typescript
await supabase.auth.signOut();
```
**4. Huidige gebruiker:**
```typescript
const { data: { user } } = await supabase.auth.getUser();
```
**Vertel:**
"Ieder van deze stuurt een request naar Supabase, krijgt een response met `{ data, error }`. Je checked of er een error is. Klaar."
---
### Slide: RLS — Row Level Security
**Vertel:**
"Tot nu toe kan iedereen alle polls zien en stemmen. Geen beperking.
RLS is een database feature: je kunt zeggen 'deze rij mag alleen de eigenaar zien' of 'iedereen mag zien, maar alleen ingelogde gebruikers mogen stemmen'.
Dit doen we met SQL policies in Supabase:
```sql
-- Iedereen mag alle polls zien
CREATE POLICY "Iedereen leest polls"
ON polls FOR SELECT
USING (true);
-- Alleen ingelogde gebruikers mogen stemmen
CREATE POLICY "Ingelogde users stemmen"
ON options FOR UPDATE
USING (auth.uid() IS NOT NULL);
```
Dit zet je in de Supabase dashboard onder Table Editor → Policies. Wij doen dit samen aan het einde."
---
---
## FASE 2: ZELF DOEN (30 minuten)
**Je rol:** Loop rond, help studenten met hun code. Toon snippets op je scherm als iemand het niet snapt.
---
### Instructies voor Studenten
**Vertel:**
"Je hebt 30 minuten. Doel: een login/signup systeem bouwen. Volg deze stappen. Veel sterkte!"
---
### Stap 1: Installeer @supabase/ssr
```bash
npm install @supabase/ssr
```
---
### Stap 2: Maak lib/supabase-server.ts
Bestand: `lib/supabase-server.ts`
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
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)
);
},
},
}
);
}
```
---
### Stap 3: Maak lib/supabase-browser.ts
Bestand: `lib/supabase-browser.ts`
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
---
### Stap 4: Maak middleware.ts
Bestand: `middleware.ts` (in root, naast package.json)
```typescript
import { NextResponse, type NextRequest } from "next/server";
import { createServerClient } from "@supabase/ssr";
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
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 }) =>
response.cookies.set(name, value, options)
);
},
},
}
);
await supabase.auth.getUser();
return response;
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
```
---
### Stap 5: Maak app/signup/page.tsx
Bestand: `app/signup/page.tsx`
```typescript
'use client'
import { useState } from 'react';
import { createSupabaseBrowserClient } from '@/lib/supabase-browser';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function SignUpPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
const supabase = createSupabaseBrowserClient();
const { error } = await supabase.auth.signUp({ email, password });
if (error) {
setError(error.message);
} else {
router.push('/');
}
};
return (
<div className="w-full max-w-md mx-auto p-4 mt-10">
<h1 className="text-2xl font-bold mb-4">Registreren</h1>
<form onSubmit={handleSignUp} className="space-y-4">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded"
required
/>
<input
type="password"
placeholder="Wachtwoord"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded"
required
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Registreer
</button>
</form>
<p className="mt-4 text-sm">
Al account? <Link href="/login" className="text-blue-500">Inloggen</Link>
</p>
</div>
);
}
```
---
### Stap 6: Maak app/login/page.tsx
Bestand: `app/login/page.tsx`
```typescript
'use client'
import { useState } from 'react';
import { createSupabaseBrowserClient } from '@/lib/supabase-browser';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
const supabase = createSupabaseBrowserClient();
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
setError(error.message);
} else {
router.push('/');
}
};
return (
<div className="w-full max-w-md mx-auto p-4 mt-10">
<h1 className="text-2xl font-bold mb-4">Inloggen</h1>
<form onSubmit={handleLogin} className="space-y-4">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded"
required
/>
<input
type="password"
placeholder="Wachtwoord"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded"
required
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Inloggen
</button>
</form>
<p className="mt-4 text-sm">
Geen account? <Link href="/signup" className="text-blue-500">Registreer</Link>
</p>
</div>
);
}
```
---
### Stap 7: Maak components/LogoutButton.tsx
Bestand: `components/LogoutButton.tsx`
```typescript
'use client'
import { createSupabaseBrowserClient } from '@/lib/supabase-browser';
import { useRouter } from 'next/navigation';
export default function LogoutButton() {
const router = useRouter();
const handleLogout = async () => {
const supabase = createSupabaseBrowserClient();
await supabase.auth.signOut();
router.push('/');
};
return (
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Uitloggen
</button>
);
}
```
---
### Stap 8: Maak components/Navbar.tsx
Bestand: `components/Navbar.tsx`
```typescript
import { createSupabaseServerClient } from '@/lib/supabase-server';
import LogoutButton from './LogoutButton';
import Link from 'next/link';
export default async function Navbar() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return (
<nav className="w-full bg-gray-800 text-white p-4">
<div className="max-w-6xl mx-auto flex justify-between items-center">
<Link href="/" className="text-xl font-bold">
PollApp
</Link>
<div className="space-x-4">
{user ? (
<>
<span className="text-sm">{user.email}</span>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="px-4 py-2 bg-blue-500 rounded hover:bg-blue-600">
Inloggen
</Link>
<Link href="/signup" className="px-4 py-2 bg-green-500 rounded hover:bg-green-600">
Registreer
</Link>
</>
)}
</div>
</div>
</nav>
);
}
```
---
### Stap 9: Update app/layout.tsx
**Voeg toe:**
```typescript
import Navbar from '@/components/Navbar';
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="nl">
<body className={geistSans.variable}>
<Navbar />
{children}
</body>
</html>
)
}
```
---
### Stap 10: Test
**Testscan:**
1. Start je app: `npm run dev`
2. Ga naar http://localhost:3000
3. Je ziet de Navbar met login/signup links
4. Ga naar /signup, registreer jezelf
5. Je word naar homepage gestuurd
6. Navbar toont je email + logout button
7. Klik logout — je bent weg
8. Ga naar /login, log in
**Alles werkt?** Prima!
---
---
## AFSLUITING (11:3012:00)
### Review
**Vertel:**
"Vandaag hebben we:
**Deel 1:**
- De in-memory polls vervangen door Supabase
- getPolls(), getPollById(), votePoll() async gemaakt
- Server Components en Client Components gemixt
- Een API route gebouwd die stemmen verwerkt
**Deel 2:**
- Auth ingebouwd met Supabase Auth
- Signup/login pagina's gemaakt
- Middleware die de sessie ververst
- LogoutButton en Navbar met gebruikersinfo
Dit is een complete, werkende app met database EN authenticatie."
---
### Huiswerk: /create Pagina
**Vertel:**
"Volgende keer bouwen we een pagina waar je zelf polls kan aanmaken. Je gaat:
1. Een form maken met vraag + 4 opties
2. De form submitten naar `/api/polls/create`
3. Die API route doet een INSERT in Supabase
4. Alleen ingelogde gebruikers mogen dit doen
Dit vereist:
- Form handling (controlledInputs)
- RLS policy in Supabase (polls: INSERT alleen voor auth.uid() IS NOT NULL)
- Error handling
Pak het rustig aan. Dit is niet moeilijker dan vandaag."
---
### Preview Volgende Les
**Vertel:**
"Les 9 gaat over het create systeem én we gaan kijken naar performance. Hoe maak je polls cachen? Hoe refresh je data zonder heel de pagina te laden? Revalidation en incremental static generation.
Tot dan!"
---
---
## TROUBLESHOOTING CHECKLIST
Als studenten vast lopen:
**"Ik krijg een 401 error"**
- Controleer of je in Supabase in Table Editor → Policies alle RLS rules hebt UIT gezet. Begin zonder, voeg rules toe als je auth werkt.
**"getPollById() geeft null terug"**
- Check of de poll ID klopt. Log de ID in de console.
- Controleer of de poll echt in de database staat.
**"Middleware draait niet"**
- Herstart je app (`npm run dev`).
- Check of `middleware.ts` in de root folder staat, niet in /app.
**"Signup werkt, maar ik kan niet inloggen"**
- Ga naar Supabase → Authentication → Users. Staat je gebruiker daar? Ja? Goed.
- Controleer je email en wachtwoord opnieuw.
- Probeer via Supabase admin panel je wachtwoord reset.
**"Cookies worden niet opgeslagen"**
- `lib/supabase-server.ts` check die setAll() functie. Ziet die er goed uit?
- Herstart de app.
question: string;
created_at: string;
options: Option[];
}
export interface Option {
id: string;
poll_id: string;
text: string;
votes: number;
}
```
---
### Stap 1.5: data.ts herschrijven met Supabase queries
**Vertel:**
> "Dit is het hart. Hier vervangen we de in-memory arrays door echte Supabase queries. De syntax is heel leesbaar: `.from('polls').select()` etc."
Open `lib/data.ts` en vervang de hele inhoud:
```typescript
import { supabase } from "./supabase";
import { Poll, Option } from "@/types";
export async function getPolls(): Promise<Poll[]> {
const { data: polls, error } = await supabase
.from("polls")
.select("*, options(*)")
.order("created_at", { ascending: false });
if (error) {
console.error("Error fetching polls:", error);
return [];
}
return polls || [];
}
export async function getPollById(id: string): Promise<Poll | null> {
const { data: poll, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) {
console.error("Error fetching poll:", error);
return null;
}
return poll;
}
export async function votePoll(
pollId: string,
optionId: string
): Promise<Option | null> {
// Haal huige votes op
const { data: option, error: fetchError } = await supabase
.from("options")
.select("votes")
.eq("id", optionId)
.single();
if (fetchError || !option) {
console.error("Error fetching option:", fetchError);
return null;
}
// Increment votes
const { data: updated, error: updateError } = await supabase
.from("options")
.update({ votes: option.votes + 1 })
.eq("id", optionId)
.select()
.single();
if (updateError) {
console.error("Error updating votes:", updateError);
return null;
}
return updated;
}
```
**Vertel:**
> "Let op drie dingen:
> 1. `select('*, options(*)')` — haalt de poll EN al zijn options in één query
> 2. `.eq('id', id)` — filteren op id
> 3. `.single()` — zeg dat je 1 rij verwacht (niet 0, niet meer dan 1)
> 4. In `votePoll`: we fetchen eerst, then +1, then update. Atomair genoeg voor demo."
---
### Stap 1.6: Homepage async maken
**Vertel:**
> "De homepage moet nu `async` zijn zodat het `await getPolls()` kan doen. React Server Components ftw."
Open `app/page.tsx` en vervang:
```typescript
import { getPolls } from "@/lib/data";
import { PollItem } from "@/components/PollItem";
import Link from "next/link";
export default async function Home() {
const polls = await getPolls();
return (
<div className="w-full p-4">
<h2 className="text-2xl font-bold mb-4">Onze polls</h2>
{polls.map((poll) => (
<Link key={poll.id} href={`/poll/${poll.id}`}>
<PollItem poll={poll} />
</Link>
))}
</div>
);
}
```
**Vertel:**
> "Geen `useState`, geen `useEffect`. Gewoon `async` + `await`. De pagina bouwt zich aan server-side op — veel sneller."
---
### Stap 1.7: PollItem aanpassen
**Vertel:**
> "De PollItem moet weten dat `option.votes` nu een number is (niet optionIndex). En `option.id` is string."
Open `components/PollItem.tsx`:
```typescript
'use client'
import { Poll, Option } from "@/types"
type PollItemProps = {
poll: Poll;
onOptionClick?: (option: Option) => void;
}
type PollItemOptionProps = {
option: Option;
percentage: number;
onClick?: (option: Option) => void;
}
export const PollItemOption = ({ option, percentage, onClick }: PollItemOptionProps) => {
return (
<div
onClick={() => onClick?.(option)}
className="relative my-2 p-3 border rounded cursor-pointer hover:bg-gray-50 overflow-hidden"
>
<div
className="absolute top-0 left-0 h-full bg-blue-100 transition-all duration-300"
style={{ width: `${percentage}%` }}
/>
<div className="relative flex justify-between">
<span>{option.text}</span>
<span className="text-gray-500">
{option.votes} ({percentage}%)
</span>
</div>
</div>
)
}
export const PollItem = ({ poll, onOptionClick }: PollItemProps) => {
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
return (
<section className="w-full my-6">
<h2 className="text-xl font-bold mb-2">{poll.question}</h2>
<p className="text-sm text-gray-500 mb-3">{totalVotes} stemmen</p>
{poll.options.map((option) => {
const percentage =
totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0;
return (
<PollItemOption
key={option.id}
option={option}
percentage={percentage}
onClick={onOptionClick}
/>
);
})}
</section>
)
}
```
**Vertel:**
> "Nu geen index meer — we werken met echte Option objects met id, text, votes. De balk kleurt blauw op basis van percentage."
---
### Stap 1.8: VoteForm aanpassen
**Vertel:**
> "De VoteForm stuurt nu `optionId` (string uuid) naar de API in plaats van `optionIndex` (number)."
Open `components/VoteForm.tsx`:
```typescript
'use client'
import { Poll, Option } from "@/types";
import { PollItem } from "./PollItem";
import { useState } from "react";
export function VoteForm({ poll: initialPoll }: { poll: Poll }) {
const [poll, setPoll] = useState(initialPoll);
const onVote = async (option: Option) => {
const response = await fetch(`/api/polls/${poll.id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ optionId: option.id }),
});
if (response.ok) {
setPoll(await response.json());
}
};
return <PollItem poll={poll} onOptionClick={onVote} />;
}
```
**Vertel:**
> "Dit is ongewijzigd van vorig weekend — behalve dat we nu echte UUIDs hebben. De vorige semaphore werkt gelijk."
---
### Stap 1.9: Detail pagina + API routes
**Vertel:**
> "De detail pagina en API routes krijgen `await` voor de async calls."
Open `app/poll/[id]/page.tsx`:
```typescript
import { getPollById } from "@/lib/data";
import { VoteForm } from "@/components/VoteForm";
import { notFound } from "next/navigation";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PollPage({ params }: PageProps) {
const { id } = await params;
const poll = await getPollById(id);
if (!poll) {
notFound();
}
return (
<div className="w-full p-4">
<h2 className="text-2xl font-bold mb-4">{poll.question}</h2>
<VoteForm poll={poll} />
</div>
);
}
```
**Vertel:**
> "Let op: `const { id } = await params;` — die await is nodig in Next.js 15. Params zijn asynchroon."
Open `app/api/polls/[id]/route.ts`:
```typescript
import { NextResponse } from "next/server";
import { getPollById, votePoll } from "@/lib/data";
interface RouteParams {
params: Promise<{ id: string }>;
}
export async function GET(request: Request, { params }: RouteParams) {
const { id } = await params;
const poll = await getPollById(id);
if (!poll) {
return NextResponse.json(
{ error: "Poll niet gevonden" },
{ status: 404 }
);
}
return NextResponse.json(poll);
}
export async function POST(request: Request, { params }: RouteParams) {
const { id } = await params;
const body = await request.json();
const { optionId } = body;
if (!optionId) {
return NextResponse.json(
{ error: "optionId is verplicht" },
{ status: 400 }
);
}
const updatedOption = await votePoll(id, optionId);
if (!updatedOption) {
return NextResponse.json(
{ error: "Kon stem niet verwerken" },
{ status: 400 }
);
}
// Terughalen volledige poll (met alle votes)
const poll = await getPollById(id);
return NextResponse.json(poll);
}
```
**Vertel:**
> "In de POST: we updaten 1 option, maar returnen de hele poll zodat de client al zijn votes ziet."
---
### Stap 1.10: Testen — Supabase is live!
**Vertel:**
> "Nou gaan we testen. Start de dev server, ga naar je homepage, klik op een poll, stem."
```bash
npm run dev
```
Open `http://localhost:3000`:
1. Homepage laadt polls van Supabase
2. Klik op een poll
3. Klik op een option
4. Votes gaan omhoog
5. **Ga naar Supabase → Table Editor → `options`:** zie de votes live stijgen!
**Vertel:**
> "Dit is het moment waarop je echte data in je database ziet veranderen. Niet meer fake arrays — echte Supabase. Coool."
---
---
## Deel 2: Supabase Auth (10:30 11:30)
### Stap 2.0: Context & installatie
**Vertel:**
> "Nu gaan we gebruikers toevoegen. Signup, login, logout. En daarna: alleen ingelogde gebruikers mogen polls maken. Supabase Auth doet dat voor ons."
```bash
npm install @supabase/ssr
```
**Vertel:**
> "De `@supabase/ssr` package geeft ons helpers speciaal voor Next.js. Server en browser clients apart."
---
### Stap 2.1: Auth helper files aanmaken
**Vertel:**
> "We maken twee files: één voor server-side auth, één voor client-side. Die hebben andere cookies dan de browser client."
Open/maak `lib/supabase-server.ts`:
```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 {
// Cookies in route handlers kunnen niet gezet worden
}
},
},
}
);
}
```
**Vertel:**
> "De server client leest/schrijft cookies. Zo blijft je sessie in stand over page reloads."
Open/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!
);
}
```
**Vertel:**
> "De browser client is voor Client Components. Ook super simpel."
---
### Stap 2.2: Auth callback route
**Vertel:**
> "Na login stuurt Supabase door naar deze callback URL. Die zet de session cookies."
Open/maak `app/auth/callback/route.ts`:
```typescript
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase-server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(new URL("/", request.url));
}
```
**Vertel:**
> "Supabase geeft een `code`, we exchange die voor een session, done. Stuur terug naar home."
---
### Stap 2.3: Middleware voor session refresh
**Vertel:**
> "De middleware refresh je session op iedere request. Zo weet je app altijd wie je bent."
Open/maak `middleware.ts` (root van je project):
```typescript
import { type NextRequest } from "next/server";
import { updateSession } from "@/lib/supabase-server";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.png|.*\\.jpg|.*\\.jpeg|.*\\.gif|.*\\.webp).*)",
],
};
```
**Vertel:**
> "De matcher zegt: op elke page behalve static files. Middleware draait dus onzichtbaar."
En voeg dit toe aan `lib/supabase-server.ts`:
```typescript
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const supabase = await createClient();
// Refresh session
await supabase.auth.getUser();
return supabaseResponse;
}
```
**Vertel:**
> "Simple: elke request trigger een `getUser()`. Dat refresh automatisch als nodig."
---
### Stap 2.4: Signup page
**Vertel:**
> "Client Component met email + password form. On submit: `supabase.auth.signUpWithPassword()`."
Open/maak `app/signup/page.tsx`:
```typescript
'use client'
import { createClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function SignupPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const supabase = createClient();
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
const { data, error: signupError } = await supabase.auth.signUpWithPassword({
email,
password,
});
if (signupError) {
setError(signupError.message);
return;
}
// Automatisch inloggen na signup (voor development)
router.push("/");
};
return (
<div className="w-full max-w-sm mx-auto p-4 mt-8">
<h1 className="text-2xl font-bold mb-4">Account aanmaken</h1>
<form onSubmit={handleSignup} className="space-y-4">
<div>
<label className="block text-sm mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block text-sm mb-1">Wachtwoord</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full p-2 border rounded"
/>
</div>
{error && <p className="text-red-600">{error}</p>}
<button
type="submit"
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700"
>
Signup
</button>
</form>
</div>
);
}
```
**Vertel:**
> "Pas op: Supabase kan een bevestigingsmail sturen. Voor development zetten we die uit (zie stap 2.5)."
---
### Stap 2.5: Email confirmation UITSCHAKELEN (development)
**Vertel:**
> "Standaard stuurt Supabase een bevestigingsmail. Daar wachten we niet op in development. Dus we zetten het uit."
1. Ga naar [supabase.com](https://supabase.com), open je project
2. Authentication → Providers → Email
3. **Zet "Confirm email" uit** (toggle naar OFF)
4. Save
**Vertel:**
> "Nu kunnen users signup zonder bevestigingsmail. Handig voor testen."
---
### Stap 2.6: Login page
**Vertel:**
> "Bijna identiek aan signup, maar dan `signInWithPassword()` in plaats van signup."
Open/maak `app/login/page.tsx`:
```typescript
'use client'
import { createClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const supabase = createClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
const { error: loginError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (loginError) {
setError(loginError.message);
return;
}
router.push("/");
};
return (
<div className="w-full max-w-sm mx-auto p-4 mt-8">
<h1 className="text-2xl font-bold mb-4">Inloggen</h1>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block text-sm mb-1">Wachtwoord</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full p-2 border rounded"
/>
</div>
{error && <p className="text-red-600">{error}</p>}
<button
type="submit"
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700"
>
Login
</button>
</form>
</div>
);
}
```
---
### Stap 2.7: LogoutButton component
**Vertel:**
> "Kleine client component die `signOut()` roept. We voegen dit later in de navbar."
Open/maak `components/LogoutButton.tsx`:
```typescript
'use client'
import { createClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
export function LogoutButton() {
const supabase = createClient();
const router = useRouter();
const handleLogout = async () => {
await supabase.auth.signOut();
router.push("/");
};
return (
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Logout
</button>
);
}
```
---
### Stap 2.8: Navbar met login state
**Vertel:**
> "Server Component die checkt: ben ik ingelogd? Zo ja: logout button. Zo nee: signup/login links."
Open/maak `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 (
<nav className="flex justify-between items-center p-4 bg-gray-100">
<Link href="/" className="text-xl font-bold">
QuickPoll
</Link>
<div className="flex gap-4">
{user ? (
<>
<span className="text-sm text-gray-600">{user.email}</span>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="text-blue-600 hover:underline">
Login
</Link>
<Link href="/signup" className="text-blue-600 hover:underline">
Signup
</Link>
</>
)}
</div>
</nav>
);
}
```
**Vertel:**
> "We gebruiken `getUser()` server-side — dat weet via cookies wie je bent. Geen Client Component nodig."
---
### Stap 2.9: Layout updaten met navbar
**Vertel:**
> "Voeg de Navbar toe aan je layout zodat die op elke pagina zichtbaar is."
Open `app/layout.tsx` en voeg toe:
```typescript
import type { Metadata } from "next";
import Navbar from "@/components/Navbar";
import "./globals.css";
export const metadata: Metadata = {
title: "QuickPoll",
description: "Polls powered by Supabase",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="nl">
<body>
<Navbar />
{children}
</body>
</html>
);
}
```
---
### Stap 2.10: RLS policies updaten
**Vertel:**
> "Nu gaan we Supabase Security Settings aan. Enkel ingelogde users mogen polls/options maken. SELECT is open (iedereen mag zien)."
Ga naar Supabase → SQL Editor en voer uit:
```sql
-- Polls: READ voor iedereen, INSERT/UPDATE/DELETE voor authenticated users
alter policy "Allow SELECT on polls for all users" on "public"."polls"
as permissive for select
to public
using ( true );
create policy "Allow INSERT on polls for authenticated users"
on "public"."polls"
as permissive for insert
to authenticated
with check ( true );
-- Options: READ voor iedereen, INSERT voor authenticated users
alter policy "Allow SELECT on options for all users" on "public"."options"
as permissive for select
to public
using ( true );
create policy "Allow INSERT on options for authenticated users"
on "public"."options"
as permissive for insert
to authenticated
with check ( true );
```
**Vertel:**
> "Veiligheidsmaatregel: niemand kan zomaar willekeurige polls deleten. Enkel authenticated users kunnen toevoegen."
---
### Stap 2.11: Create poll page
**Vertel:**
> "Ingelogde users kunnen hier nieuwe polls aanmaken. Form voelt gelijk aan de vorige — email/wachtwoord was ook een form."
Open/maak `app/create/page.tsx`:
```typescript
'use client'
import { createClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function CreatePollPage() {
const [question, setQuestion] = useState("");
const [options, setOptions] = useState(["", ""]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createClient();
const handleAddOption = () => {
setOptions([...options, ""]);
};
const handleOptionChange = (index: number, value: string) => {
const updated = [...options];
updated[index] = value;
setOptions(updated);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
// Validatie
if (!question.trim()) {
setError("Vraag is verplicht");
setLoading(false);
return;
}
const validOptions = options.filter((opt) => opt.trim());
if (validOptions.length < 2) {
setError("Minimaal 2 opties nodig");
setLoading(false);
return;
}
try {
// 1. Insert poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({ question })
.select()
.single();
if (pollError || !poll) {
setError("Fout bij aanmaken poll");
return;
}
// 2. Insert options
const pollOptions = validOptions.map((text) => ({
poll_id: poll.id,
text,
votes: 0,
}));
const { error: optionsError } = await supabase
.from("options")
.insert(pollOptions);
if (optionsError) {
setError("Fout bij aanmaken opties");
return;
}
// Success — ga naar homepage
router.push("/");
} catch (err) {
setError("Onverwachte fout");
} finally {
setLoading(false);
}
};
return (
<div className="w-full max-w-md mx-auto p-4 mt-8">
<h1 className="text-2xl font-bold mb-4">Nieuwe poll</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm mb-1">Vraag</label>
<input
type="text"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Waar ga je op vakantie?"
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block text-sm mb-2">Opties</label>
{options.map((option, idx) => (
<input
key={idx}
type="text"
value={option}
onChange={(e) => handleOptionChange(idx, e.target.value)}
placeholder={`Optie ${idx + 1}`}
className="w-full p-2 border rounded mb-2"
/>
))}
<button
type="button"
onClick={handleAddOption}
className="text-sm text-blue-600 hover:underline"
>
+ Optie toevoegen
</button>
</div>
{error && <p className="text-red-600">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? "Bezig..." : "Poll aanmaken"}
</button>
</form>
</div>
);
}
```
**Vertel:**
> "Deze page is NIET beveiligd — technisch kan iedereen naar `/create` gaan. Maar Supabase RLS zorgt dat INSERT failt als je niet ingelogd bent. We gaan dit beveiligen met middleware voor volgende les."
---
### Stap 2.12: Link naar create page in navbar
**Vertel:**
> "Voeg een 'Nieuw Poll' button toe die alleen zichtbaar is voor ingelogde users."
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 (
<nav className="flex justify-between items-center p-4 bg-gray-100">
<Link href="/" className="text-xl font-bold">
QuickPoll
</Link>
<div className="flex gap-4 items-center">
{user ? (
<>
<span className="text-sm text-gray-600">{user.email}</span>
<Link
href="/create"
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Nieuw Poll
</Link>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="text-blue-600 hover:underline">
Login
</Link>
<Link href="/signup" className="text-blue-600 hover:underline">
Signup
</Link>
</>
)}
</div>
</nav>
);
}
```
---
### Stap 2.13: Testen — auth werkt!
**Vertel:**
> "Nu gaan we het hele flow testen. Signup, login, create poll, stem, logout."
1. **Ga naar localhost:3000 → navbar ziet je geen user**
2. **Klik "Signup"**
- Email: `test@example.com`
- Password: `password123`
- Klik signup
- Redirect naar homepage
3. **Navbar toont nu jouw email + "Logout" en "Nieuw Poll"**
4. **Klik "Nieuw Poll"**
- Vraag: "Welk framework?"
- Optie 1: Next.js
- Optie 2: React
- Optie 3: Svelte
- Klik "Poll aanmaken"
5. **Homepage toont je nieuwe poll! Stem erop.**
6. **Klik logout**
7. **Navbar reset — geen email meer**
8. **Klik login, gebruik dezelfde credentials**
9. **Je bent terug ingelogd**
**Vertel:**
> "Dit is magic. Jouw account bestaat nu echt in Supabase. Je wachtwoord is gehasht. Cookies houden je sessie in stand. Goedendag, echte app!"
---
### Stap 2.14: Check Supabase — data is live
**Vertel:**
> "Ga naar Supabase → Table Editor. Controleer:"
1. **Auth Users → zie je account**
2. **Polls table → zie je nieuw poll**
3. **Options table → zie je 3 opties met votes**
**Vertel:**
> "Dit alles zit nu in Supabase. Als je app crashed, data is veilig. If je 100 users hebt, 1 database. Mooi systeem."
---
---
## Afsluiting (11:30 12:00)
### Dingen om stil te staan
**Vertel:**
> "Jullie hebben vandaag 2 grote stappen gemaakt:
> 1. **Supabase Database Connection:** from arrays naar echte SQL. Votes zijn persistent.
> 2. **Supabase Auth:** Users, signup/login/logout, server vs browser clients. RLS om data te beveiligen.
>
> Next week gaan we auth-only routes beveiligen met middleware. En error handling solider maken."
### Huiswerk
- Experimenteer: maak 3 accounts, create 5 polls, stem erop
- Vraag: "Wat gebeurt er als je `/create` bezoekt zonder ingelogd te zijn?"
- Antwoord: De page laadt, maar Supabase RLS rejecteert de INSERT. We catchen die error (stap 2.11).
- Bonus: maak een "Trendende polls" query (TOP 5 op votes)
### Q&A
Wacht op vragen. Typische topics:
- "Hoe zit die session refresh in middleware?" → `getUser()` triggert auto-refresh als token stale is
- "Is dit veilig?" → Basis ja, RLS helps, maar app-logic (authorization) moet ook checked worden
- "Kan ik auth tokens gebruiken?" → Ja! Supabase kan via `Authorization: Bearer` tokens werken. Volgende keer.
---
## Snelle Referentie (Clipboard)
### Environment variables
```
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxx
```
### Key imports
```typescript
// Server
import { createClient } from "@/lib/supabase-server";
// Browser
import { createClient } from "@/lib/supabase-browser";
// Both
import { supabase } from "@/lib/supabase";
```
### Common queries
```typescript
// Get all
const { data } = await supabase.from("table").select();
// Get one
const { data } = await supabase.from("table").select().eq("id", id).single();
// Insert
const { data } = await supabase.from("table").insert(record).select();
// Update
const { data } = await supabase.from("table").update(changes).eq("id", id);
// With relations
const { data } = await supabase.from("polls").select("*, options(*)");
```
### Auth
```typescript
// Signup
await supabase.auth.signUpWithPassword({ email, password });
// Login
await supabase.auth.signInWithPassword({ email, password });
// Get user
const { data: { user } } = await supabase.auth.getUser();
// Logout
await supabase.auth.signOut();
```
---
**End of Les 8 — Live Coding Guide**