← Terug naar de blog
BeveiligingScanaris Team · 7 min lezen · October 03, 2026

Supabase-beveiliging: wat je moet nakijken voordat je lanceert

Supabase geeft je in minuten een database en een API, en dat is precies het gevaar: de API staat vanaf het eerste moment open. Als je de beveiliging niet instelt, kan iedereen met je publieke key je tabellen lezen en beschrijven. Deze gids loopt, in de volgorde waarin je het het beste doet, alles langs wat je moet nakijken voordat je je app voor de wereld openzet: RLS en policies met voorbeeld-SQL, welke key waar hoort, Storage, views, de Auth-instellingen en hoe je aan het eind controleert dat het echt dicht zit.

Hoe Supabase je data blootstelt (en waarom dat goed is)

Voor je Postgres zit een laag (PostgREST) die elke tabel in het schema public omzet in een HTTP-endpoint. Je app in de browser praat er rechtstreeks mee, met een publieke key. Dat scheelt je een backend, maar het betekent dat er geen server van jou tussen zit die rechten controleert: de Row Level Security-policies zijn je backend. Kloppen ze, dan is het ontwerp solide; ontbreken ze, dan is de hele tabel één URL verwijderd.

1. RLS aan op ALLE tabellen

Row Level Security is de belangrijkste barrière van Supabase. Zonder kan de publieke key hele tabellen rechtstreeks vanuit de browser opvragen. De klassieke vibe coder-fout: snel een nieuwe tabel aanmaken, of de AI die laten aanmaken, en vergeten RLS aan te zetten. Dat was precies de oorzaak van het Lovable-lek (CVE-2025-48757).

Loop tabel voor tabel na in de Table Editor: alles met "RLS disabled" staat open voor het publiek. Om niet op je ogen te hoeven vertrouwen, geeft deze query in de SQL Editor de ontbrekende tabellen:

select c.relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;

En voor elke tabel:

alter table public.tabelnaam enable row level security;

Aanzetten alleen is niet genoeg: een tabel met RLS aan maar zonder policies weigert alles (je app werkt niet meer tot je ze schrijft), en een tabel met een te ruime policy beschermt niets.

2. Policies die echt weergeven wie wat mag

Een policy using (true) voor iedereen laat de tabel open, ook met RLS aan; AI-assistenten stellen ze vaak voor "zodat het werkt". Schrijf concrete policies en splits de regels per operatie, want soms wil je lezen toestaan maar verwijderen niet.

Patroon A: elke gebruiker zijn eigen rijen

create policy "notes: eigen lezen" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "notes: eigen aanmaken" on public.notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "notes: eigen bewerken" on public.notes
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

De with check is wat voorkomt dat iemand een rij invoegt of bewerkt namens een andere gebruiker. auth.uid() in een (select …) zetten zorgt ervoor dat Postgres het één keer per query evalueert in plaats van één keer per rij; op grote tabellen is het verschil enorm. Zet een index op user_id.

Patroon B: lidmaatschap van een team of organisatie

create policy "projects: teamleden" on public.projects
  for select to authenticated
  using (
    exists (
      select 1 from public.team_members m
      where m.team_id = projects.team_id
        and m.user_id = (select auth.uid())
    )
  );

De tabel team_members heeft ook eigen policies nodig, en liefst mag alleen een beheerder erin invoegen.

Patroon C: publieke inhoud, alleen lezen

create policy "posts: publiek leesbaar" on public.posts
  for select to anon, authenticated
  using (published = true);

Dat is legitiem. Wat er niet mag zijn: een open schrijf-policy of kolommen met persoonsgegevens in een publiek leesbare tabel.

3. anon, service_role en de nieuwe keys

De anon-key is publiek en hoort in de frontend: hij is bedoeld om door RLS in toom te worden gehouden. De service_role-key omzeilt RLS volledig en heeft totale toegang. Hij mag nooit, maar dan ook nooit, in de browser of in een variabele met NEXT_PUBLIC_, VITE_ of NUXT_PUBLIC_ verschijnen: dat voorvoegsel betekent "dit wordt naar de client gestuurd".

Supabase heeft bovendien een nieuw paar keys geïntroduceerd met namen die geen twijfel laten: sb_publishable_… (voor de browser, onder RLS) en sb_secret_… (alleen server, omzeilt RLS, apart intrekbaar). Begin je vandaag een project, gebruik dan deze; heb je een ouder project, migreer dan zodra je kunt. Gebruik de geheime key alleen op de server, in Edge Functions of vertrouwde backends, en vermoed je een lek, roteer hem dan meteen vanuit het dashboard.

4. Storage: buckets hebben ook regels

Bestandsopslag gebruikt eigen policies, op de tabel storage.objects. Een publieke bucket serveert elk bestand via URL aan iedereen die hem heeft, voor altijd. Bepaal bewust welke buckets publiek zijn (avatars, productafbeeldingen) en bescherm de rest. Een voorbeeld zodat elke gebruiker alleen in zijn eigen map kan lezen en uploaden:

create policy "documents: eigen map" on storage.objects
  for all to authenticated
  using (
    bucket_id = 'documents'
    and (storage.foldername(name))[1] = (select auth.uid())::text
  )
  with check (
    bucket_id = 'documents'
    and (storage.foldername(name))[1] = (select auth.uid())::text
  );

Upload privédocumenten naar privébuckets en serveer ze met kortlevende ondertekende URL's, niet naar een publieke bucket met moeilijk te raden namen.

5. Views, functies en blootgestelde schema's

Drie plekken waar data weglekt, ook als de tabellen in orde zijn:

  • Views. In Postgres draait een view met de rechten van degene die hem aanmaakte, dus een view op een beschermde tabel kan rijen teruggeven die de policies zouden verbieden. Maak ze aan met with (security_invoker = true) (Postgres 15 of nieuwer) of loop ze één voor één na.
  • RPC-functies. Een security definer-functie draait met de rechten van de eigenaar. Gebruik hem alleen als het echt nodig is, valideer de parameters erin en beperk wie hem mag aanroepen met revoke execute … from anon.
  • Blootgestelde schema's. Onder Settings → API kies je welke schema's de API publiceert. Wat niet via HTTP naar buiten hoeft, hoort niet op die lijst.

6. Auth-instellingen die je moet nakijken

  • E-mailbevestiging aan, zodat niemand accounts registreert met andermans adres.
  • Limieten op pogingen voor inloggen en e-mail versturen: ze staan standaard aan; controleer dat niemand ze "om te testen" heeft opgeschroefd.
  • Redirect-URL's alleen toegestaan naar je eigen domeinen; een open lijst maakt het mogelijk je login te misbruiken om naar phishingsites door te sturen.
  • OAuth-providers: alleen die je gebruikt, met het toestemmingsscherm op naam van je merk.
  • Vervaltijd van magic links en OTP's kort; de standaardwaarde is redelijk, rek hem niet op.

7. Secrets en keys uit de frontend

Controleer naast de geheime Supabase-key dat er geen keys van derden (Stripe, e-mail, AI) in de client-JavaScript zitten. De AI zet ze daar soms neer om iets snel werkend te krijgen, en ze blijven voor altijd zichtbaar in de bundle. Aanroepen naar betaalde diensten lopen via een Edge Function of je server, nooit vanuit de browser. Zie de secrets die je app in de browser lekt.

8. Omgevingen: ontwikkeling is geen productie

Gebruik een apart Supabase-project voor ontwikkeling, met verzonnen data. Een testproject met een kopie van de echte data en half afgemaakte policies ligt net zo bloot als productie, en niemand houdt het in de gaten.

Zo controleer je dat het dicht zit (de eindtest)

  • Incognitotest. Kopieer met de app open vanuit F12 → Network een aanroep naar /rest/v1/ en open hem zonder sessie. Hij moet een lege lijst of een fout teruggeven.
  • "Meer vragen"-test. Haal ingelogd het filter uit de URL (?user_id=eq.…). Alleen jouw rijen mogen terugkomen.
  • Twee-accountstest. Maak twee gebruikers aan en bevestig dat geen van beiden de data van de ander ziet, bewerkt of verwijdert, Storage-bestanden incluis.
  • Security Advisor. In het dashboard somt de sectie Advisors tabellen zonder RLS, te ruime policies en blootgestelde functies op. Houd hem groen.

Scanaris kijkt je Supabase voor je na

Scanaris bevat een optionele Supabase-audit die, met een read-only connection string, tabel voor tabel controleert of RLS aanstaat en of er policies zijn, en in de normale scan keys in de frontend, te ruime CORS en endpoints die data zonder sessie serveren detecteert. Elke bevinding komt met een prompt die je zo in je AI plakt. Draai hem voor de lancering en je weet of je een deur open hebt laten staan; zet monitoring aan en hij waarschuwt je als de AI er in een nieuwe tabel weer een opent.

Veelgestelde vragen

Kan ik RLS overslaan door er een eigen backend voor te zetten?

Dat kan, door vanaf je server alleen de geheime key te gebruiken en de publieke nooit in de client. Maar dan moet je backend elke rechtencontrole doen die RLS zou doen. Voor de meeste apps is RLS minder werk en moeilijker te breken.

Kost RLS prestaties?

Nauwelijks, als de policies goed geschreven zijn: (select auth.uid()) in plaats van auth.uid(), indexen op de kolommen die de policies gebruiken en geen zware subquery's. Een slecht geschreven policy kan een grote tabel wel degelijk vertragen.

En de tabellen die de AI na de lancering aanmaakt?

Die worden zonder RLS geboren. Neem "zet RLS aan en schrijf de policies" op in je routine (of in de prompt van je AI) telkens als er een nieuwe tabel verschijnt, en gebruik een automatische herscan om erachter te komen als het vergeten wordt.

Hoe staat je site ervoor?

Laat Scanaris er even overheen gaan en check het in 30 seconden, gratis.

Mijn site scannen

Gerelateerde checks

Beveiligingsgidsen per stack