Database Schema
Volledig PostgreSQL schema met Row Level Security (RLS) policies.
Tabellen
profielen
CREATE TABLE public.profielen (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
voornaam TEXT NOT NULL,
achternaam TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
telefoon TEXT NOT NULL, -- 06-nummer (E.164: +316...)
wa_opt_in BOOLEAN DEFAULT FALSE, -- expliciete WhatsApp toestemming
email_opt_in BOOLEAN DEFAULT TRUE,
aangemaakt_op TIMESTAMPTZ DEFAULT NOW(),
verwijder_op TIMESTAMPTZ, -- ingesteld op opening + 30d
CONSTRAINT telefoon_formaat CHECK (telefoon ~ '^\+316[0-9]{8}$')
);
Relatie: Uitbreiding op Supabase auth.users
RLS: Gebruiker ziet alleen zichzelf
cirkels
CREATE TABLE public.cirkels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
naam TEXT NOT NULL,
beschrijving TEXT,
mentor_id UUID NOT NULL REFERENCES public.profielen(id),
uitnodigings_code TEXT UNIQUE NOT NULL, -- 8-karakter random code
opening_datum DATE NOT NULL,
opening_verzonden BOOLEAN DEFAULT FALSE,
cirkel_sleutel TEXT, -- AES-256 sleutel, alleen zichtbaar NA opening_datum
aangemaakt_op TIMESTAMPTZ DEFAULT NOW(),
gesloten_op TIMESTAMPTZ, -- na vernietiging
max_leden INT DEFAULT 50,
CONSTRAINT opening_in_toekomst CHECK (opening_datum > aangemaakt_op::DATE)
);
RLS:
- SELECT: alleen voor leden
- INSERT: iedereen (wordt mentor)
- UPDATE: alleen mentor
- cirkel_sleutel: NULL totdat opening_datum voorbij
lidmaatschappen
CREATE TABLE public.lidmaatschappen (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cirkel_id UUID NOT NULL REFERENCES public.cirkels(id) ON DELETE CASCADE,
gebruiker_id UUID NOT NULL REFERENCES public.profielen(id) ON DELETE CASCADE,
rol TEXT NOT NULL DEFAULT 'lid' CHECK (rol IN ('mentor', 'lid')),
toegevoegd_op TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (cirkel_id, gebruiker_id)
);
Relatie: Joins cirkels ↔ profielen RLS: Leden zien alleen hun eigen lidmaatschappen
notities
CREATE TABLE public.notities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cirkel_id UUID NOT NULL REFERENCES public.cirkels(id) ON DELETE CASCADE,
schrijver_id UUID NOT NULL REFERENCES public.profielen(id),
ontvanger_id UUID NOT NULL REFERENCES public.profielen(id),
versleutelde_tekst TEXT NOT NULL, -- AES-256 encrypted, base64
iv TEXT NOT NULL, -- initialisatievector, base64
geschreven_op TIMESTAMPTZ DEFAULT NOW(),
gelezen_op TIMESTAMPTZ, -- null totdat blik open is
CONSTRAINT niet_aan_zichzelf CHECK (schrijver_id != ontvanger_id)
);
Encryptie: versleutelde_tekst en iv zijn base64-encoded
RLS:
- Schrijver mag metadata zien (voor wie, wanneer)
- Ontvanger mag inhoud lezen NA opening
- Schrijver mag eigen notities zien (NIET de versleutelde tekst)
uitnodigingen
CREATE TABLE public.uitnodigingen (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cirkel_id UUID NOT NULL REFERENCES public.cirkels(id),
uitgenodigd_email TEXT NOT NULL,
uitgenodigd_door UUID NOT NULL REFERENCES public.profielen(id),
token TEXT UNIQUE NOT NULL, -- éénmalig gebruik
verlopen_op TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days',
gebruikt_op TIMESTAMPTZ,
aangemaakt_op TIMESTAMPTZ DEFAULT NOW()
);
Validatie: token moet uniek zijn, verlopen_op standaard 7 dagen
RLS: Uitgenodigd-door-persoon mag zien/wijzigen
verwijderlogs
CREATE TABLE public.verwijderlogs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cirkel_id UUID, -- kan null zijn na verwijdering
actie TEXT NOT NULL,
details JSONB,
uitgevoerd_op TIMESTAMPTZ DEFAULT NOW()
);
Audit trail: Geanonimiseerde log voor data-vernietiging RLS: Alleen service-role key mag schrijven (cron jobs)
Row Level Security (RLS) Policies
Alle tabellen hebben RLS ingeschakeld:
ALTER TABLE profielen ENABLE ROW LEVEL SECURITY;
ALTER TABLE cirkels ENABLE ROW LEVEL SECURITY;
ALTER TABLE lidmaatschappen ENABLE ROW LEVEL SECURITY;
ALTER TABLE notities ENABLE ROW LEVEL SECURITY;
ALTER TABLE uitnodigingen ENABLE ROW LEVEL SECURITY;
PROFIELEN
-- Gebruiker ziet alleen zichzelf
CREATE POLICY "eigen_profiel" ON profielen
FOR ALL USING (auth.uid() = id);
CIRKELS
-- Zichtbaar voor leden
CREATE POLICY "cirkel_voor_leden" ON cirkels
FOR SELECT USING (
EXISTS (
SELECT 1 FROM lidmaatschappen
WHERE cirkel_id = cirkels.id
AND gebruiker_id = auth.uid()
)
);
-- Aanmaken: iedereen (wordt mentor)
CREATE POLICY "cirkel_aanmaken" ON cirkels
FOR INSERT WITH CHECK (mentor_id = auth.uid());
-- Bewerken: alleen mentor
CREATE POLICY "cirkel_bewerken_mentor" ON cirkels
FOR UPDATE USING (mentor_id = auth.uid());
KRITISCH: Cirkel-Sleutel Timing
-- cirkel_sleutel is NULL totdat opening_datum verstreken is
-- Edge Function onthult sleutel pas op openingsdatum
CREATE POLICY "sleutel_pas_na_opening" ON cirkels
FOR SELECT USING (
CASE
WHEN opening_datum <= CURRENT_DATE THEN TRUE
ELSE (cirkel_sleutel IS NULL) -- geeft NULL terug, nooit vroegtijdig
END
);
NOTITIES
-- Schrijven: alleen leden, vóór opening
CREATE POLICY "notitie_schrijven" ON notities
FOR INSERT WITH CHECK (
schrijver_id = auth.uid()
AND EXISTS (
SELECT 1 FROM lidmaatschappen
WHERE cirkel_id = notities.cirkel_id
AND gebruiker_id = auth.uid()
)
AND EXISTS (
SELECT 1 FROM cirkels
WHERE id = notities.cirkel_id
AND opening_datum > CURRENT_DATE -- blik is nog dicht
)
);
-- Lezen: ontvanger NA opening; schrijver mag metadata zien
CREATE POLICY "notitie_lezen_na_opening" ON notities
FOR SELECT USING (
ontvanger_id = auth.uid()
AND EXISTS (
SELECT 1 FROM cirkels
WHERE id = notities.cirkel_id
AND opening_datum <= CURRENT_DATE
)
);
-- Schrijver: metadata zien, NIET versleutelde tekst
CREATE POLICY "notitie_meta_schrijver" ON notities
FOR SELECT USING (schrijver_id = auth.uid());
UITNODIGINGEN
CREATE POLICY "uitnodiging_mentor" ON uitnodigingen
FOR ALL USING (
uitgenodigd_door = auth.uid()
AND EXISTS (
SELECT 1 FROM cirkels
WHERE id = uitnodigingen.cirkel_id
AND mentor_id = auth.uid()
)
);
Indexes (Performance)
CREATE INDEX idx_lidmaatschappen_cirkel_id ON lidmaatschappen(cirkel_id);
CREATE INDEX idx_lidmaatschappen_gebruiker_id ON lidmaatschappen(gebruiker_id);
CREATE INDEX idx_notities_cirkel_id ON notities(cirkel_id);
CREATE INDEX idx_notities_ontvanger_id ON notities(ontvanger_id);
CREATE INDEX idx_cirkels_mentor_id ON cirkels(mentor_id);
CREATE INDEX idx_profielen_email ON profielen(email);
Migrations
Alle tabellen worden via migrations aangemaakt:
supabase/migrations/001_schema.sql— Tabellensupabase/migrations/002_rls.sql— RLS policiessupabase/migrations/003_functions.sql— Stored procedures & triggers
Zie: Deployment voor hoe migrations uit te voeren.
Opmerking over veiligheid: RLS is je laatste verdedigingslinie. Test altijd dat RLS juist werkt voordat je deployt!