Gids

    Supabase RLS: zo beveilig je je database met row level security

    Supabase RLS uitgelegd: hoe je row level security aanzet, per bewerking een policy schrijft, test of je regels kloppen en de gaten dicht die AI-tools openlaten.

    Wat doet RLS in Supabase?

    Supabase zet een API direct op je Postgres-database. Je app praat daarmee via de publishable key (in oudere projecten de anon key), en die sleutel staat gewoon in je browsercode. Iedereen die je site opent, kan hem vinden en zelf verzoeken naar je database sturen.

    Row level security (RLS) is de Postgres-functie die bepaalt wat zo'n verzoek mag. Per tabel schrijf je regels, policies, die per rij beslissen of iemand hem mag lezen, toevoegen, wijzigen of verwijderen. Postgres voegt die regels zelf aan elke query toe. Vraagt iemand om alle bestellingen, dan krijgt hij alleen de rijen waar de policy ja op zegt.

    RLS is in Supabase dus geen extra slot op de deur, het is de deur zelf. Zonder RLS is de publieke sleutel genoeg om een hele tabel uit te lezen. Dat is verreweg de meest voorkomende bevinding in apps die met een AI-tool zijn gebouwd.

    Liever eerst de uitleg zonder code? Lees row level security in Supabase uitgelegd.

    Supabase RLS aanzetten, en waarom je het nooit uitzet

    Maak je een tabel in de Table Editor van het dashboard, dan staat RLS standaard aan. Maak je hem met SQL of een migratie, en dat doen Lovable, Cursor en Bolt vrijwel altijd, dan staat RLS uit tot je het zelf aanzet:

    alter table public.todos enable row level security;
    
    -- Welke tabellen in public hebben nog geen RLS?
    select tablename
    from pg_tables
    where schemaname = 'public' and rowsecurity = false;
    • RLS uit: iedereen met de publieke sleutel kan alles in de tabel, ook zonder in te loggen.
    • RLS aan zonder policy: niemand komt erin via de API, ook je eigen app niet. Veilig, maar je app toont dan niets.
    • RLS aan met policies: wie mag, mag precies wat de policy toestaat, en verder niemand.

    Zet RLS dus ook niet 'even' uit om een foutmelding weg te krijgen. Die melding betekent meestal dat er een policy ontbreekt. Zonder RLS verdwijnt hij, samen met je beveiliging, en omdat je sleutel publiek is, is 'even' al lang genoeg.

    De rollen anon en authenticated

    Elk verzoek naar je database krijgt een Postgres-rol mee. In een policy bepaal je met 'to' voor welke rol de regel geldt:

    • anon: een bezoeker die niet is ingelogd en alleen de publieke sleutel gebruikt.
    • authenticated: iemand die is ingelogd. Let op: dat is iedereen die een account kan aanmaken, niet alleen je klanten. 'to authenticated using (true)' betekent dus: open voor iedereen die zich registreert.
    • service_role: de rol achter de secret key (vroeger de service role key). Die negeert RLS helemaal en hoort alleen op de server, nooit in je frontend of je repo.

    In policies gebruik je auth.uid(): het id van de ingelogde gebruiker uit zijn token. Voor een bezoeker zonder account is dat leeg, dus een regel als 'auth.uid() = user_id' houdt niet-ingelogde bezoekers vanzelf buiten.

    Een RLS-policy schrijven: één regel per bewerking

    Stel: je hebt een tabel todos met een kolom user_id die naar de gebruiker verwijst. Elke gebruiker mag alleen zijn eigen taken zien en beheren. Dan schrijf je per bewerking een policy:

    -- Lezen: alleen je eigen rijen
    create policy "Eigen taken lezen"
    on public.todos for select
    to authenticated
    using ( (select auth.uid()) = user_id );
    
    -- Toevoegen: alleen rijen op je eigen naam
    create policy "Eigen taken toevoegen"
    on public.todos for insert
    to authenticated
    with check ( (select auth.uid()) = user_id );
    
    -- Wijzigen: alleen je eigen rijen, en ze blijven van jou
    create policy "Eigen taken wijzigen"
    on public.todos for update
    to authenticated
    using ( (select auth.uid()) = user_id )
    with check ( (select auth.uid()) = user_id );
    
    -- Verwijderen: alleen je eigen rijen
    create policy "Eigen taken verwijderen"
    on public.todos for delete
    to authenticated
    using ( (select auth.uid()) = user_id );
    • using bepaalt welke bestaande rijen iemand mag zien, wijzigen of verwijderen.
    • with check bepaalt hoe een nieuwe of gewijzigde rij eruit mag zien. Zonder with check bij update kan iemand zijn taak op de naam van een ander zetten.
    • Een update werkt alleen als er ook een select-policy is: Postgres moet de rij eerst kunnen lezen.
    • Zet auth.uid() tussen (select ...) en leg een index op user_id. Dan rekent Postgres het id één keer uit in plaats van per rij, wat bij grote tabellen veel scheelt.

    Veelgemaakte fouten in RLS-policies

    • using (true). Voelt als beveiliging, maar laat iedereen alles zien. Koppel een policy altijd aan auth.uid() of aan een rol.
    • Rollen in de profieltabel. Staat 'admin' in een tabel die de gebruiker zelf mag bijwerken, dan zet iemand zichzelf op admin. Gebruik een aparte rollentabel.
    • Rechten uit user_metadata. Die gegevens kan de gebruiker zelf aanpassen, dus ze horen niet in een policy. Gebruik app_metadata of een eigen tabel.
    • Views zonder security_invoker. Een view negeert standaard de RLS van de tabellen eronder. Maak views met 'with (security_invoker = true)'.
    • Security definer-functies in een publiek schema. Zo'n functie draait met de rechten van de maker en slaat RLS over. Zet hem in een schema dat niet via de API bereikbaar is en geef hem een vaste search_path.

    Een admin-rol controleren met een security definer-functie

    Moet een beheerder alles kunnen zien, maak dan een aparte rollentabel zonder policies (dus onbereikbaar via de API) en een functie die de rol van de ingelogde gebruiker opzoekt. Supabase raadt aan zo'n functie in een eigen schema te zetten:

    create schema if not exists private;
    
    -- RLS aan en geen policies: via de API kan niemand hierbij
    create table public.user_roles (
      user_id uuid primary key references auth.users on delete cascade,
      role text not null check (role in ('user', 'admin'))
    );
    alter table public.user_roles enable row level security;
    
    create function private.is_admin()
    returns boolean
    language sql
    security definer
    set search_path = ''
    stable
    as $$
      select exists (
        select 1 from public.user_roles
        where user_id = (select auth.uid()) and role = 'admin'
      );
    $$;
    
    revoke execute on function private.is_admin() from public;
    grant usage on schema private to authenticated;
    grant execute on function private.is_admin() to authenticated;
    
    create policy "Admins lezen alle taken"
    on public.todos for select
    to authenticated
    using ( (select private.is_admin()) );

    Omdat de functie security definer is, mag hij de rollentabel lezen terwijl de gebruiker dat zelf niet kan. Met search_path = '' voorkom je dat iemand er via een eigen schema een andere tabel onder schuift. Postgres combineert policies voor dezelfde bewerking met 'of': een admin ziet alles, een gewone gebruiker nog steeds alleen zijn eigen taken.

    Supabase RLS testen: zo weet je of het klopt

    Een policy die er goed uitziet, kan nog steeds lekken. Test daarom op drie manieren:

    • De Security Advisor in je dashboard (onder Advisors). Die meldt onder meer tabellen zonder RLS, RLS zonder policies, policies die altijd waar zijn, views die RLS omzeilen en policies die user_metadata gebruiken.
    • Als gebruiker in SQL. Met 'set local role' en 'set local request.jwt.claim.sub' doe je binnen een transactie een bezoeker of een ingelogde gebruiker na, en zie je precies wat die te zien krijgt.
    • Automatisch met pgTAP. De Supabase CLI draait met 'supabase test db' alle tests uit de map supabase/tests/database, zodat elke policy bij elke wijziging opnieuw wordt gecontroleerd.

    Een RLS-test met pgTAP

    Deze test controleert dat een bezoeker zonder account niets ziet en dat een ingelogde gebruiker geen taken van anderen ziet. Zet hem in supabase/tests/database/todos_rls.test.sql, zorg voor testdata met taken van minstens twee gebruikers, en draai supabase test db. pgTAP zet je eenmalig aan met 'create extension if not exists pgtap with schema extensions'.

    begin;
    select plan(2);
    
    -- Niet ingelogd: geen enkele taak zichtbaar
    set local role anon;
    select is_empty(
      'select * from public.todos',
      'anon ziet geen taken'
    );
    
    -- Ingelogd als testgebruiker: geen taken van anderen
    set local role authenticated;
    set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111';
    select is_empty(
      $$ select * from public.todos
         where user_id <> '11111111-1111-1111-1111-111111111111' $$,
      'gebruiker ziet geen taken van anderen'
    );
    
    select * from finish();
    rollback;

    De test eindigt met rollback, dus hij laat niets achter in je database.

    Buiten RLS: sleutels, edge functions en storage

    RLS beschermt je tabellen, maar niet alles. Deze gaten laten AI-tools ook vaak open:

    • De secret key of service role key in je frontend, in een meegebouwd .env-bestand of in een publieke repo. Zoek in je project op 'service_role' en 'sb_secret_', en roteer de sleutel als hij ooit publiek is geweest.
    • Sleutels in je git-historie. Weghalen uit het huidige bestand is niet genoeg; alleen roteren lost het op.
    • Edge functions zonder controle. Controleer per functie of JWT-verificatie aanstaat en of de functie de aanroeper echt valideert.
    • Publieke storage buckets. Uploads van gebruikers in een publieke bucket zijn voor iedereen te raden en te downloaden. Zet de bucket op privé; ook storage werkt met RLS-policies, op de tabel storage.objects.

    Bekijk wat je doet als een sleutel gelekt is als je secret key openbaar is geweest.

    Supabase beveiligen: de checklist

    In het kort, en in deze volgorde:

    • Zet RLS aan op elke tabel in het publieke schema.
    • Geef elke tabel een policy per bewerking, gekoppeld aan auth.uid() of een rol.
    • Zoek naar 'using (true)' en 'to authenticated' zonder eigenaarscontrole.
    • Haal rollen uit de profieltabel en gebruik nooit user_metadata in een policy.
    • Zet security_invoker op je views en je security definer-functies in een eigen schema.
    • Houd de secret key uit je frontend en je repo, en roteer hem als hij ooit publiek was.
    • Controleer elke edge function op authenticatie en zet storage buckets op privé.
    • Draai de Security Advisor en je pgTAP-tests bij elke wijziging, niet één keer.

    Gebruik de Supabase security checklist voor je livegang en vergelijk een scan of pentest voor verdere controle.

    Veelgestelde vragen over Supabase RLS

    Wat is RLS in Supabase?

    RLS (row level security) is een functie van Postgres waarmee je per tabel regels schrijft die per rij bepalen wie hem mag lezen, toevoegen, wijzigen of verwijderen. Omdat de publieke sleutel van Supabase in je browsercode staat, is RLS wat je data echt beschermt.

    Hoe zet ik RLS aan in Supabase?

    In de Table Editor staat het bij nieuwe tabellen standaard aan. Voor tabellen die met SQL of een migratie zijn gemaakt, voer je 'alter table <tabel> enable row level security' uit. Daarna schrijf je per bewerking een policy, anders kan je app er zelf ook niet meer bij.

    Mag ik RLS tijdelijk uitzetten tijdens het bouwen?

    Liever niet. Je publieke sleutel staat in je browsercode, dus zolang RLS uit staat, is de tabel voor iedereen open. Krijg je een foutmelding met RLS aan, dan ontbreekt er meestal een policy. Voeg die toe in plaats van RLS uit te zetten.

    Wat is het verschil tussen anon en authenticated?

    anon is een bezoeker die niet is ingelogd, authenticated is iemand die wel is ingelogd. Authenticated is dus iedereen die een account kan aanmaken. Een policy 'to authenticated' zonder controle op auth.uid() is daarom bijna even open als een policy voor anon.

    Wat is het verschil tussen using en with check?

    using bepaalt welke bestaande rijen iemand mag zien, wijzigen of verwijderen. with check bepaalt hoe een nieuwe of gewijzigde rij eruit mag zien. Bij insert gebruik je with check, bij select en delete using, en bij update allebei.

    Omzeilt de service role key RLS?

    Ja. De service role key, in nieuwere projecten de secret key, slaat alle policies over. Hij hoort daarom alleen op de server. Staat hij in je frontend of in een publieke repo, dan is je database in feite open en moet je hem direct roteren.

    Hoe test ik mijn RLS-policies?

    Draai eerst de Security Advisor in het dashboard. Doe daarna in SQL een gebruiker na met 'set local role' en 'set local request.jwt.claim.sub', en leg de controles vast in pgTAP-tests die je met 'supabase test db' bij elke wijziging draait.

    Is Supabase veilig?

    Supabase zelf is een volwassen platform met sterke beveiliging. De risico's zitten vrijwel altijd in de configuratie van je eigen project: tabellen zonder row level security, te ruime policies en sleutels op de verkeerde plek.

    Mag de anon key in mijn frontend staan?

    Ja. De anon key, in nieuwere projecten de publishable key, is bedoeld om publiek te zijn. Hij is alleen veilig als row level security op elke tabel aanstaat, want die bepaalt wat iemand met die sleutel mag.

    Wat gebeurt er als row level security uitstaat?

    Dan kan iedereen die je publieke sleutel uit je website haalt de hele tabel uitlezen, en afhankelijk van je rechten ook aanpassen of verwijderen.

    Hoe weet ik of mijn AI-gebouwde app dit goed heeft staan?

    Meestal niet door ernaar te kijken. Koppel je repository aan een scanner die de rechten, sleutels en functies per wijziging nagaat, zodat je het niet handmatig hoeft te onthouden.

    Laat het automatisch nakijken

    VibeShield koppelt aan je GitHub-repo en controleert je databaserechten, sleutels en packages — bij elke push opnieuw, met een pull request die het dichtzet.

    Start met scannen
    Terug naar de site