"Supabase-beveiliging" betekent voor een met AI gebouwde app twee verschillende dingen. Het eerste is de projectconfiguratie: wat Supabase standaard blootstelt en wat je zelf moet dichtzetten. Het tweede is wat Lovable, Bolt of Cursor met Supabase doen wanneer ze de code genereren, en daar komen bijna alle gaten vandaan. Deze gids behandelt beide, met drie SQL-query's om je project in een minuut door te lichten, een reviewprompt voor je AI en de manier om van buitenaf te controleren dat alles dicht is.
Deel 1: hoe beveiliging werkt in Supabase
De data-API is bewust publiek
Supabase zet voor je Postgres een REST-API (PostgREST) die de browser rechtstreeks aanroept met de anon-sleutel (of de nieuwere publishable-sleutel). Die sleutel zit in het JavaScript van al je gebruikers en dat is normaal: hij identificeert het project, hij geeft geen rechten. Wat bepaalt wie welke rij mag lezen of schrijven, is Row Level Security. De andere sleutel, service_role (of secret), omzeilt RLS volledig en mag alleen op een server leven. Als je deze scheiding begrijpt, begrijp je 80% van de Supabase-beveiliging.
RLS en policies: de enige echte barrière
Een nieuwe tabel in het schema public is via de API bereikbaar zodra je hem aanmaakt. Zonder ingeschakelde RLS leest en schrijft iedereen met de anon-sleutel alle rijen. Met RLS aan en zonder policies komt niemand binnen. Met policies komt binnen wie zij toelaten. Vandaar de regel: RLS op elke tabel en één expliciete policy per bewerking (select, insert, update, delete). De concrete patronen (elke gebruiker zijn eigen rijen, teamlidmaatschap, publieke alleen-lezen inhoud) staan in wat je in Supabase controleert vóór de lancering.
Wat RLS omzeilt zonder dat je het merkt
- Views. Standaard draait een view met de rechten van wie hem aanmaakte, en negeert hij dus de RLS van de tabellen die hij bevraagt. Maak ze aan met
with (security_invoker = on). - Security definer-functies. Een zo gemarkeerde RPC-functie draait als haar eigenaar en leest wat ze wil. Gebruik ze alleen als het echt nodig is, met
set search_path = '', en trek het uitvoerrecht vananonin als een bezoeker ze niet mag aanroepen. - Blootgestelde schema's. Alles in
public(tabellen, views en functies) is via de API bereikbaar. Is een functie intern, verplaats hem dan naar een ander schema. - De service_role-sleutel. In de frontend, in een publieke repository of in een prompt die je in een AI plakt, betekent hij volledige controle over de database.
Auth: instellingen die een blik verdienen
Onder Authentication vind je de e-mailbevestiging (laat die aan), de lijst toegestane Redirect URLs (is die leeg of met wildcards, dan kan een magic link op een vreemd domein belanden), de rate limits, captcha voor registratie en herstel, het wachtwoordbeleid, bescherming tegen gelekte wachtwoorden en anoniem inloggen (uit, tenzij je het bewust gebruikt). Elk is een vinkje; geen enkele staat standaard fout, maar de AI zet ze graag om zodat "de tests slagen".
Storage, Realtime en Edge Functions
Publieke buckets leveren elk bestand aan wie de URL heeft: gebruik voor uploads van gebruikers private buckets, policies op storage.objects en ondertekende URL's met een vervaltijd, plus een groottelimiet en toegestane types per bucket. In Realtime respecteren Postgres-wijzigingen RLS, maar broadcast- en presence-kanalen hebben eigen policies nodig als ze privédata vervoeren. In Edge Functions gaan geheimen via supabase secrets set, nooit in de code, en als je de JWT-verificatie voor een webhook uitschakelt, valideer dan zelf de handtekening van de afzender.
Het project zelf
De directe Postgres-connectiestring (met het databasewachtwoord) mag je server of je omgevingsvariabelen nooit verlaten. Schakel SSL-afdwinging in, overweeg netwerkbeperkingen bij een betaald plan, en bekijk af en toe de Security Advisor van het dashboard (Database, Advisors): die meldt tabellen zonder RLS, security definer-views, functies met een veranderbaar search_path en blootgestelde auth-tabellen.
Deel 2: wat de AI verkeerd doet met Supabase
Dit zijn de fouten die het vaakst terugkomen in apps die met Lovable, Bolt of Cursor zijn gegenereerd, op volgorde van frequentie:
- Tabellen aanmaken zonder RLS. De generator schrijft de migratie, de app werkt en niemand schakelt iets in. Het is de oorsprong van Lovable's CVE-2025-48757, die data van honderden projecten blootlegde.
using (true)-policies. Als RLS iets blokkeert, "repareert" de AI het met een policy die alles doorlaat. Dat is uitgeschakelde RLS onder een andere naam.- service_role in de browser. Een query faalt op rechten en de snelle oplossing is de sleutel wisselen. Zoek nu meteen naar
service_rolein je bundle. - Security definer-functies om fouten te omzeilen. Zelfde patroon: in plaats van de juiste policy te schrijven, wordt de query in een functie gewikkeld die als superuser draait.
- Publieke buckets voor gebruikersbestanden. Avatars, prima; facturen, documenten of exports, nee.
- Privilegevelden die de gebruiker kan bewerken. Een
roleofis_adminin de tabelprofilesmet een update-policy die die kolommen niet uitsluit. Iedereen maakt zichzelf admin vanuit de browserconsole. - E-mailbevestiging of limieten uitschakelen zodat tests sneller lopen, en dat vergeten in productie.
Drie SQL-query's om je project door te lichten
Plak ze in de SQL-editor van het dashboard. De eerste toont publieke tabellen zonder RLS; idealiter geeft hij niets terug:
select n.nspname as schema, c.relname as table
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r' and n.nspname = 'public' and not c.relrowsecurity;
De tweede vindt policies die alles doorlaten:
select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true');
Zo'n policy op een tabel met publieke alleen-lezen inhoud kan correct zijn; op profiles, orders of messages niet. De derde toont functies die met de rechten van hun eigenaar draaien:
select p.proname, pg_get_function_identity_arguments(p.oid) as args
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef;
Elk resultaat zou een reden moeten hebben die je kunt uitleggen. Zo niet, haal dan de security definer weg of verplaats de functie uit public.
Een reviewprompt vóór het deployen
Plak hem in Lovable, Bolt of Cursor met toegang tot je schema en je code:
Beoordeel de beveiliging van deze Supabase-app en verander nog niets. Antwoord met een lijst:
1. Tabellen in het schema public zonder ingeschakelde RLS.
2. Policies met using (true) of with check (true), en of ze gerechtvaardigd zijn.
3. Privilegekolommen (role, is_admin, plan, credits) die een gebruiker met de huidige update-policies kan wijzigen.
4. Views zonder security_invoker en security definer-functies, met de reden voor elk.
5. Elk gebruik van de service_role-sleutel of de Postgres-connectiestring buiten de server.
6. Publieke buckets met gebruikersbestanden.
7. Uitgeschakelde Auth-instellingen (e-mailbevestiging, limieten, captcha) en redirect URLs met wildcards.
Stel voor elk punt de exacte SQL of wijziging voor en wacht op mijn bevestiging.
Controleer het resultaat van buitenaf
De definitieve test is niet het dashboard, maar wat de API antwoordt aan een vreemde. Met je project-URL en de anon-sleutel (beide staan in de browsercode, dus een aanvaller heeft ze ook):
curl -s "https://JOUW_PROJECT.supabase.co/rest/v1/profiles?select=*" -H "apikey: JOUW_ANON_KEY" -H "Authorization: Bearer JOUW_ANON_KEY"
Komen er rijen terug, dan staat die tabel open. Komt er [] of een rechtenfout, dan werkt RLS. Herhaal dit voor elke gevoelige tabel en met een gewone ingelogde gebruiker om te bevestigen dat hij alleen zijn eigen data ziet. Scanaris doet deze controle voor je: het detecteert de Supabase-URL en -sleutel in je bundle, test de gebruikelijke tabellen zonder sessie, zoekt de service_role-sleutel en de connectiestring in het JavaScript, controleert waar je app het sessietoken opslaat en geeft je voor elke bevinding een fix prompt. Heeft Lovable je app gegenereerd, bekijk dan ook de beveiligingsgids voor Lovable; zijn de sleutels in de client het probleem, dan geheimen die je app in de browser lekt.
Veelgestelde vragen
Is het een probleem dat de anon-sleutel in de browser zit?
Nee. Hij is daarvoor ontworpen en identificeert alleen het project. Wat de data beschermt, is RLS. Wat nooit in de browser mag, is service_role (of de nieuwe secret-sleutel).
Veranderen de nieuwe publishable- en secret-sleutels iets?
Ze veranderen het formaat en laten zich roteren zonder het JWT aan te raken, maar het model blijft hetzelfde: publishable is publiek en gaat door RLS; secret omzeilt het en leeft op de server. De oude sleutels blijven werken tot je ze uitschakelt.
Als Lovable of Bolt mijn app heeft gebouwd, moet ik dan nog steeds het Supabase-dashboard in?
Ja. De tool genereert het schema, maar RLS, policies, buckets en Auth-instellingen zijn van jou. Tien minuten met de drie query's hierboven en de Security Advisor vertellen je in welke staat het project is.