7.1 KiB
Les 10 — Supabase Auth & Row Level Security: Verdieping
Lesstof
Vervolg op Les 9. We bouwen de auth-flow uit tot productie-kwaliteit en zetten RLS goed in. Aan het einde is je QuickPoll-app multi-user veilig: elke gebruiker kan alleen zijn eigen polls bewerken en verwijderen, anderen kunnen meelezen en stemmen.
1. Recap Les 9
Aan het einde van Les 9 had je:
signUp,signInWithPassword,signOutserver actions- Navbar met sessie-status
- Middleware die de sessie ververst
- Eén basale RLS-policy op
polls
Vandaag gaan we daar veel verder mee. Vier onderwerpen: de drie auth-methodes in detail, sessies onder de motorkap, RLS in detail, en beschermde routes via twee patronen.
2. De drie auth-methodes
2.1 E-mail + wachtwoord
Wat we in Les 9 gebruikten. Vereist e-mailverificatie in productie. Code is simpel, UX is klassiek.
2.2 Magic link
Geen wachtwoord, gebruiker krijgt een link in zijn e-mail.
await supabase.auth.signInWithOtp({
email: "user@example.com",
options: { emailRedirectTo: "https://app.com/auth/callback" }
});
Voordeel: gebruiker hoeft geen wachtwoord te onthouden, geen "wachtwoord vergeten"-flow. Nadeel: gebruiker moet altijd zijn e-mail openen.
2.3 OAuth (Google, GitHub, Discord, etc.)
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: "https://app.com/auth/callback" }
});
Voordeel: één klik. Nadeel: gebruiker moet account hebben bij die provider.
Wanneer welke?
- B2B SaaS → e-mail + wachtwoord of magic link
- Consumer app → OAuth (Google/Apple)
- Internal tools → magic link
- High-security → e-mail + wachtwoord + MFA
3. Hoe een sessie écht werkt
Een Supabase-sessie bestaat uit twee tokens:
| Token | Levensduur | Gebruik |
|---|---|---|
| Access token (JWT) | 1 uur | Elke API-call |
| Refresh token | 30 dagen | Nieuwe access token aanvragen |
De flow:
- Login → server stuurt beide tokens terug, opgeslagen in cookies
- Elke request stuurt het access token mee
- Na 1 uur: access token verlopen, refresh token genereert een nieuwe
- Na 30 dagen of na logout: opnieuw inloggen
@supabase/ssr regelt stap 3 automatisch in de middleware.
Wat zit er in een JWT?
{
"sub": "abc-uuid-van-gebruiker",
"email": "user@example.com",
"role": "authenticated",
"exp": 1734567890
}
sub is de user-ID. Postgres leest dit uit voor auth.uid() in je RLS-policies.
4. Server- vs. client-componenten
In Next.js 16 App Router heb je twee plekken waar je auth gebruikt:
Server component (default)
import { createSupabaseServerClient } from "@/lib/supabase-server";
export default async function Page() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return <div>Hallo {user.email}</div>;
}
Client component ("use client")
"use client";
import { createBrowserClient } from "@supabase/ssr";
const supabase = createBrowserClient(URL, KEY);
export function LogoutButton() {
return <button onClick={() => supabase.auth.signOut()}>Logout</button>;
}
Vuistregel: doe alles in server components tenzij je interactie nodig hebt (een knop, een form-event).
5. Row Level Security in detail
RLS draait in Postgres zelf. Je server action vraagt data op, en Postgres beslist wat je terugkrijgt — niet je code.
using vs. with check
| Clausule | Gebruikt bij | Wat het doet |
|---|---|---|
using |
SELECT, UPDATE, DELETE | Welke rijen zie/raak je? |
with check |
INSERT, UPDATE | Mag de NIEUWE waarde bestaan? |
Voorbeeld: voorkomen dat een gebruiker een poll claimt die van iemand anders is:
create policy "owner can update"
on polls for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
using zegt: je mag de rij bewerken als je de eigenaar bent.
with check zegt: na de update moet je nog steeds de eigenaar zijn (anders kun je user_id overschrijven).
Vier basis-policies voor je polls tabel
-- 1. Iedereen mag polls lezen
create policy "polls public select"
on polls for select
using (true);
-- 2. Ingelogde gebruikers mogen polls aanmaken
create policy "auth users can insert"
on polls for insert
with check (auth.uid() is not null and auth.uid() = user_id);
-- 3. Alleen eigenaar mag updaten
create policy "owner can update"
on polls for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- 4. Alleen eigenaar mag verwijderen
create policy "owner can delete"
on polls for delete
using (auth.uid() = user_id);
6. Beschermde routes: twee patronen
Patroon 1 — middleware redirect
Voor hele route-groepen (bijv. alle /app/*):
// middleware.ts
const { data: { user } } = await supabase.auth.getUser();
if (!user && request.nextUrl.pathname.startsWith("/create")) {
return NextResponse.redirect(new URL("/login", request.url));
}
Snel, gecentraliseerd. Nadeel: je kunt geen page-specifieke UX tonen ("log in om dit te zien" met preview).
Patroon 2 — server component check
Voor pagina-specifieke logica:
export default async function MyPollsPage() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return <LoginPrompt />;
const { data: polls } = await supabase
.from("polls")
.select("*")
.eq("user_id", user.id);
return <PollList polls={polls} />;
}
Gebruik patroon 1 voor "deze hele sectie is privé", patroon 2 voor "deze pagina werkt anders per user".
7. Belangrijk: dubbele veiligheid
Best practice = check op twee niveaus:
- Middleware zegt: anonieme bezoeker mag deze route niet zien
- RLS zegt: zelfs als de bezoeker er komt, ziet/wijzigt hij niks dat niet van hem is
Waarom? Als ooit één laag faalt (bug, vergeten check, security update), is de andere er nog. Diepte-verdediging.
8. Veelvoorkomende fouten
| Fout | Oplossing |
|---|---|
| Policy werkt niet | RLS aan? alter table X enable row level security; |
auth.uid() is null in policy |
Cookies komen niet door — middleware check |
| Anoniem account kan ineens INSERTen | with check ontbreekt op insert-policy |
| User kan andermans rij stelen via update | with check op update vergeten |
| Service-role-key in browser | NOOIT. Gebruik anon-key in browser, service-role alleen op server |
9. Checklist einde les
- 4 RLS-policies actief op
polls(select, insert, update, delete) - 2 RLS-policies actief op
options(select-iedereen, insert-eigenaar-van-poll) - "My Polls" pagina toont alleen polls van ingelogde gebruiker
- Edit + Delete knoppen werken alleen voor eigenaar
- Middleware-bescherming op
/createen/my-polls - Werkende auth-flow getest met 2 verschillende accounts
10. Vooruitblik
In Les 11 verlaten we QuickPoll en starten met de Vercel AI SDK. Je app moet op dat moment auth + RLS op productie-niveau hebben — dat is de basis waar je AI-features veilig op kunt bouwen.