Files
novi-lessons/Les18-Supabase-Auth-RLS/Les18-Lesstof.md
2026-06-03 16:58:25 +02:00

14 KiB

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
  2. Supabase Auth — drie methoden
  3. Magic link flow
  4. Social login (OAuth)
  5. Session in Next.js App Router
  6. Middleware — sessie refresh
  7. RLS — wat en waarom
  8. RLS policies in praktijk
  9. Protected routes + server checks
  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.

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).

User vult email in, krijgt link, klikt → ingelogd.

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".

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.


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:

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

"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
<button onClick={signInWithGitHub}>Login met GitHub</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

pnpm add @supabase/supabase-js @supabase/ssr

Vier files

utils/supabase/client.ts — voor Client Components:

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:

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:

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):

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

// 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

alter table tasks enable row level security;
create policy "select own" on tasks for select using (auth.uid() = user_id);

Now:

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 <jwt> 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

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:

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:

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)

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 <div>Welkom {user.email}</div>;
}

Protected layout

app/(authenticated)/layout.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

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

"use client";
import { createClient } from "@/utils/supabase/client";

async function logout() {
  const supabase = createClient();
  await supabase.auth.signOut();
  window.location.href = "/login";
}

<button onClick={logout}>Uitloggen</button>

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