Supabase RLS controleren: kan iedereen je tabellen lezen?
Vraag je tabellen op met de publieke key uit je eigen frontend, zonder in te loggen, en daarna met het account van een tweede testgebruiker. Krijg je rijen terug die niet van die gebruiker zijn, dan staat de tabel open, ook als het dashboard zegt dat RLS aanstaat. Kijk daarna naar je opslag: bestanden in een openbare bucket kan iedereen met de link downloaden.
Een tabel die openstaat, zie je niet in je app en niet in je dashboard. Je ziet hem pas als je kijkt zoals een buitenstaander: met de key uit je frontend, zonder account, of met het account van iemand anders. Hieronder doe je dat, met de verzoeken en de SQL erbij.
Wat Row Level Security is en hoe je een policy schrijft, staat in Row Level Security uitgelegd. Deze gids is de controle: of wat je hebt ingesteld ook werkt, per rol en per tabel.
Herken je dit?
- Je app is met Lovable, Bolt of v0 op Supabase gebouwd, en je hebt de policies nooit zelf gelezen.
- In het dashboard staat bij elke tabel dat RLS aanstaat, maar je weet niet wat de policies toestaan.
- Iedereen kan zich met een e-mailadres aanmelden, en je hebt nooit gekeken wat een vers account te zien krijgt.
- Gebruikers uploaden bestanden: facturen, foto’s, contracten.
- Een AI-builder heeft ooit een fout als “permission denied” voor je opgelost, en je weet niet hoe.
Je eigen app is de slechtste test
Als je je app test, ben je ingelogd als jezelf, en je frontend vraagt alleen jouw rijen op. Ergens in de code staat een filter als .eq('gebruiker_id', user.id). Het scherm klopt dus. Of de database ook rijen van anderen zou geven, zie je niet, want de app vraagt er niet om. Een bezoeker met een eigen verzoek laat dat filter gewoon weg.
De SQL-editor in het dashboard misleidt op een andere manier. Daar werk je als eigenaar van de database, en voor de eigenaar gelden je policies niet. Je ziet er altijd alles, of de tabel nu dicht zit of niet. En een policy kan er staan en toch alles toestaan: het dashboard toont dat er een regel is, niet wat die regel doet.
Zo controleer je het, rol voor rol
1. Pak de URL en de key die je frontend gebruikt
Open je live app, open de ontwikkelaarstools van je browser (F12) en kies het tabblad Netwerk. Klik iets aan dat gegevens laadt. Je ziet verzoeken naar een adres als abcd1234.supabase.co/rest/v1/. In de headers staat bij apikey de key die elke bezoeker heeft.
Vergelijk hem met de keys in de API-instellingen van je project. Het hoort de publishable key te zijn (begint met sb_publishable_) of de oude anon-key. Staat er een secret key (sb_secret_) of de service_role-key, stop dan hier. Met die key gelden geen policies, dus de tests hieronder zeggen niets, en de key is uitgelekt. Wat je dan doet: API-key gelekt.
2. Laat de Security Advisor eerst kijken
In het dashboard staat onder Advisors de Security Advisor. Hij vindt tabellen zonder RLS, policies die altijd waar zijn en openbare buckets waarvan je de bestanden kunt opsommen. Los eerst op wat hij als error markeert. Of je regels passen bij wat jouw app doet, test hij niet.
3. Vraag elke tabel op als bezoeker zonder account
# Met een publishable key (begint met sb_publishable_)
curl "https://abcd1234.supabase.co/rest/v1/klanten?select=*&limit=5" \
-H "apikey: sb_publishable_JOUWKEY"
# Met een oude anon-key (begint met eyJ): stuur hem ook als Bearer mee
curl "https://abcd1234.supabase.co/rest/v1/klanten?select=*&limit=5" \
-H "apikey: eyJhbGciOi..." \
-H "Authorization: Bearer eyJhbGciOi..."Krijg je namen, e-mailadressen of bestellingen terug, dan kan iedereen ze ophalen. Een lege lijst [] of een fout met permission denied is goed, zolang er wel rijen in de tabel staan. Doe dit voor elke tabel in het schema public, ook voor tabellen die je app nergens toont.
4. Probeer als tweede gebruiker bij de eerste te komen
Maak twee testaccounts, A en B, en geef A via de app een bestelling. Log daarna als B in via de API en probeer met het token van B de rijen van A te lezen en te wijzigen.
# 1. Log in als B en kopieer access_token uit het antwoord
curl -X POST "https://abcd1234.supabase.co/auth/v1/token?grant_type=password" \
-H "apikey: sb_publishable_JOUWKEY" \
-H "Content-Type: application/json" \
-d '{"email": "test-b@jouwdomein.nl", "password": "wachtwoord-van-b"}'
# 2. Lees als B de bestellingen van A
curl "https://abcd1234.supabase.co/rest/v1/bestellingen?select=*&gebruiker_id=eq.ID-VAN-A" \
-H "apikey: sb_publishable_JOUWKEY" \
-H "Authorization: Bearer TOKEN-VAN-B"
# 3. Wijzig als B een bestelling van A
curl -X PATCH "https://abcd1234.supabase.co/rest/v1/bestellingen?id=eq.BESTELLING-VAN-A" \
-H "apikey: sb_publishable_JOUWKEY" \
-H "Authorization: Bearer TOKEN-VAN-B" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"status": "geannuleerd"}'Je wilt twee keer [] zien. Komt de bestelling van A terug, dan kan elke ingelogde gebruiker bij die van iedereen. Het id van A staat in het dashboard onder Authentication.
Heeft je app een kolom als rol of is_admin, test dan ook of B die bij zichzelf kan aanpassen, met hetzelfde PATCH-verzoek op zijn eigen profiel en {"rol": "admin"} als inhoud. Lukt dat, dan kan iedereen zichzelf beheerder maken. Haal het recht om die kolom te wijzigen weg:
revoke update on profielen from anon, authenticated;
grant update (naam, telefoon) on profielen to authenticated;5. Herhaal het in de SQL-editor met een andere rol
In de SQL-editor van Supabase kun je een query draaien als een andere rol: anon, of authenticated als een bepaalde gebruiker. Kies die rol in het rolmenu van de editor in plaats van de standaard. Werk je met psql of een andere databaseclient, dan doet de SQL hieronder hetzelfde. Door de rollback aan het eind blijft er niets veranderd.
begin;
set local role anon;
select count(*) from klanten; -- hoort 0 te zijn
rollback;
begin;
set local role authenticated;
set local request.jwt.claim.sub = 'ID-VAN-B';
select gebruiker_id, count(*) from bestellingen group by gebruiker_id;
update bestellingen set status = 'test' where gebruiker_id = 'ID-VAN-A';
delete from bestellingen where gebruiker_id = 'ID-VAN-A';
rollback;Bij de tweede select hoort één regel terug te komen: die van B. Bij de update en de delete wil je UPDATE 0 en DELETE 0 zien. Elk getal boven nul is een rij van A die B kan aanpassen of weghalen.
6. Zoek policies die te veel toestaan
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
and (qual = 'true' or with_check = 'true');
select tablename, policyname, qual, with_check
from pg_policies
where qual ilike '%user_meta%' or with_check ilike '%user_meta%';Kijk bij elke regel van de eerste query naar de kolom roles. Staat daar {anon} of {public}, dan geldt de policy ook voor bezoekers zonder account; bij {authenticated} voor iedereen met een account. Voor gepubliceerde producten kan dat kloppen, voor klanten en bestellingen nooit. Elke regel van de tweede query is een fout; waarom, staat bij de valkuilen.
7. Controleer je opslag
Openbare buckets slaan de policies over voor downloaden: wie de link heeft, krijgt het bestand. En bestandsnamen zijn vaak te raden, zoals facturen/2026-0042.pdf.
select id, public from storage.buckets;
select policyname, cmd, roles, qual
from pg_policies
where schemaname = 'storage' and tablename = 'objects';
curl -X POST "https://abcd1234.supabase.co/storage/v1/object/list/documenten" \
-H "apikey: sb_publishable_JOUWKEY" \
-H "Content-Type: application/json" \
-d '{"prefix": "", "limit": 20}'Facturen, contracten en kopieën van identiteitsbewijzen horen in een bucket die niet openbaar is, met een policy die elke gebruiker alleen zijn eigen map geeft. Gebruik je de oude anon-key, zet dan bij het laatste verzoek ook de Authorization-regel uit stap 3.
create policy "eigen map lezen"
on storage.objects for select
to authenticated
using (
bucket_id = 'documenten'
and (storage.foldername(name))[1] = auth.uid()::text
);Waar het bij de controle misgaat
- Ingelogd behandelen als vertrouwd. Een policy
to authenticated using (true)klinkt streng, maar staat aanmelden open, dan is iedereen met een e-mailadres ingelogd. Kijk in de instellingen van Auth of nieuwe gebruikers zich zelf mogen aanmelden, en schrijf policies op eigendom, niet op ingelogd zijn. - De rol uit user_metadata halen. Een gebruiker kan zijn eigen user_metadata vanuit de browser aanpassen. Een policy die daar
adminin zoekt, laat iedereen beheerder worden. Zet rollen in app_metadata, die alleen de server kan schrijven, of in een eigen tabel die gebruikers niet mogen wijzigen. - Alleen de tabellen testen die je kent. Een AI-builder maakt onderweg hulptabellen aan voor meldingen, logs of uitnodigingen. Test elke tabel die de Table Editor toont.
- Een view of functie die om je policies heen werkt. Een view draait standaard met de rechten van wie hem maakte, en een functie met
security definerook. Via/rest/v1/rpc/kan een bezoeker zo’n functie aanroepen. Zet bij viewsalter view naam_van_view set (security_invoker = true);en geef functies alleen de rechten die ze nodig hebben. De Security Advisor markeert allebei.
Wanneer je hulp nodig hebt
Vind je een tabel die openstaat en weet je niet welke policies je app nodig heeft, of zit de logica in functies en views die je niet kunt lezen, laat het dan nakijken. In de securitycheck doen we deze tests voor elke tabel en elke rol, en zetten we dicht wat openstaat. Moet de app daarna ook rollen, betalingen of een beheerscherm krijgen, dan hoort dat bij je AI-app laten afmaken.
Stonden er persoonsgegevens open, dan kan dat een datalek zijn; wanneer je dat moet melden, lees je bij de Autoriteit Persoonsgegevens. Staat je project in een Amerikaanse regio, lees dan je app naar Europese hosting en wat AI-tools niet voor je regelen. Andere fouten uit AI-builders staan in je website is met AI gebouwd, de overige gidsen bij AI-apps afmaken en beveiligen.
Veelgestelde vragen
Mijn verzoek geeft een lege lijst terug. Is de tabel dan veilig?
Kan ik dit testen zonder mijn live gegevens te raken?
Hoe vaak moet ik dit doen?
Verder lezen
Een app die je met Lovable, Bolt, v0, Cursor of Claude bouwde: dichtzetten, afmaken en online houden.
- Securitycheck voor je AI-app
- Een vaste check van je app uit Lovable, Bolt, v0, Cursor of Claude Code: databaserechten, keys, inloggen, webhooks en de AVG-basis. Met een rapport en de fixes.
- AI-app laten afmaken
- Je prototype uit Lovable, Bolt, v0, Cursor of Claude Code werkt, maar is nog niet klaar voor klanten. Wij bouwen de rest: betalingen, rollen, e-mail, beheer, hosting en onderhoud.
- API-key gelekt
- Een key van OpenAI, Anthropic, Supabase of Stripe in je frontend of je git-geschiedenis: intrekken, vervangen, limiet erop.
- Werkt niet na deploy
- Een witte pagina, een 404 bij verversen of een inloglink naar localhost: de vijf oorzaken en hoe je ze een voor een uitsluit.
- Lovable-app zelf hosten
- Van Lovable naar je eigen GitHub, domein en hosting, met of zonder eigen Supabase-project. Wat er meekomt, wat achterblijft, en waar Bolt en v0 anders werken.
Wij testen je database als buitenstaander
De kennismaking is gratis. We reageren binnen één werkdag.




