Waarom RLS belangrijk is voor SaaS
Zonder RLS is je API de enige barrière tussen een gebruiker en elke rij in je database. Een onbedoelde "haal alles op"-query en je hebt een datalek.
Met RLS dwingt PostgreSQL toegangsregels af bij het uitvoeren van query's. Ongeacht wat je API stuurt, retourneert de database alleen rijen die de geverifieerde gebruiker mag zien. Dit is verdediging in de diepte, je API en database dwingen toegangscontrole onafhankelijk van elkaar af.
RLS activeren
Activeer RLS op elke gebruikersgerichte tabel. Tabellen zonder RLS-beleidsregels zijn standaard wijd open (als ze worden benaderd via de service-rolsleutel) of volledig ontoegankelijk (als ze worden benaderd via de anon-sleutel zonder beleidsregels).
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
Wanneer geactiveerd worden standaard geen rijen geretourneerd totdat je beleidsregels maakt. Dit is de correcte beveiligingspositie.
Het basispatroon voor gebruikerseigendom
Het meest voorkomende patroon, gebruikers kunnen alleen hun eigen rijen zien en wijzigen: CREATE POLICY "Users can view own rows" ON projects FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own rows" ON projects FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own rows" ON projects FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own rows" ON projects FOR DELETE USING (auth.uid() = user_id);
Multi-tenant workspace beleidsregels
Voor SaaS met teams en workspaces controleer je workspace-lidmaatschap: CREATE POLICY "Workspace members can view projects" ON projects FOR SELECT USING ( workspace_id IN ( SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid() ) );
Dit controleert de workspace_members-koppeltabel bij elke query. Indexeer workspace_members(user_id, workspace_id) voor prestaties.
Rolgebaseerde toegangscontrole
Voeg rolcontrole toe voor beheerdersbewerkingen: CREATE POLICY "Only admins can delete workspace projects" ON projects FOR DELETE USING ( workspace_id IN ( SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid() AND role = 'admin' ) );
Sla rollen op in je workspace_members-tabel (role text DEFAULT 'member'). Rollen: owner, admin, member, viewer.
Veelgemaakte fouten te vermijden
1. Vergeten RLS te activeren op nieuwe tabellen. Maak een checklist: elke nieuwe tabel krijgt RLS geactiveerd voordat deze wordt verbonden met de frontend.
2. De service-rolsleutel gebruiken in de frontend. De service-rolsleutel omzeilt RLS. Stel hem nooit bloot aan gebruikers, gebruik de anon-sleutel in WeWeb/FlutterFlow en de service-rol alleen in server-side Xano-functies.
3. Geen beleid voor INSERT. Veel ontwikkelaars voegen SELECT- en UPDATE-beleidsregels toe maar vergeten INSERT. Zonder dat kunnen anon-sleutelgebruikers helemaal geen rijen aanmaken.
4. N+1-beleidszoekopdrachten. Als je beleid een grote tabel JOIN't bij elke query, zie je prestatieproblemen op schaal. Materialiseer lidmaatschapszoekopdrachten of gebruik indexen agressief.
RLS-beleidsregels vanaf nul schrijven
Elk RLS-beleid heeft drie componenten: de tabel waarop het van toepassing is, de bewerking die het regelt (SELECT, INSERT, UPDATE, DELETE, of ALL), en de expressie die voor elke rij evalueert naar waar of onwaar. De USING-clausule is van toepassing op rijen die worden gelezen of gewijzigd, ze voert een controle uit tegen bestaande rijen. De WITH CHECK-clausule geldt alleen voor INSERT en UPDATE, ze valideert de nieuwe rij die wordt weggeschreven.
Een veelvoorkomende bron van verwarring: je hebt aparte USING- en WITH CHECK-expressies nodig voor UPDATE-beleidsregels. USING bepaalt welke rijen de gebruiker kan bijwerken (welke rijen hij kan vinden), en WITH CHECK bepaalt welke waarden hij in die rijen mag schrijven. Wil je dat gebruikers hun eigen rijen kunnen bijwerken maar niet het user_id-veld kunnen wijzigen, dan heeft je UPDATE-beleid nodig: USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id). Zonder de WITH CHECK zou een gebruiker een rij die hij bezit kunnen bijwerken en user_id instellen op het ID van een andere gebruiker, waarmee hij effectief het eigendom overdraagt.
Schrijf voor complexe beleidsregels met subqueries (workspace-lidmaatschapscontroles, opzoekingen in rechtentabellen) de subquery eerst apart uit en test deze als een standalone SELECT voordat je hem in een beleid inbedt. PostgreSQL's EXPLAIN ANALYZE is hier je vriend, voer het uit op een beleidsexpressie om te zien of deze indexen gebruikt of terugvalt op een sequentiële scan.
Veelvoorkomende RLS-patronen
Patroon 1: gebruiker bezit rij. Het eenvoudigste en meest voorkomende. De user_id-kolom verwijst naar auth.users. USING (auth.uid() = user_id). Werkt voor notities, documenten, persoonlijke instellingen.
Patroon 2: team- of workspace-toegang. Een workspace_members-koppeltabel bepaalt de toegang. USING (workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())). Werkt voor alle SaaS met gedeelde resources.
Patroon 3: publiek lezen, eigenaar schrijft. Contentplatforms waar iedereen kan lezen maar alleen de auteur kan bewerken. SELECT-beleid: USING (true) of USING (published = true). UPDATE/DELETE-beleid: USING (auth.uid() = author_id). Werkt voor blogs, directories, marketplaces.
Patroon 4: admin-uitzondering. Admins kunnen alle rijen in een workspace zien, ongeacht andere eigendomsregels. Voeg een OR-clausule toe: USING (auth.uid() = user_id OR auth.uid() IN (SELECT user_id FROM workspace_members WHERE role = 'admin' AND workspace_id = projects.workspace_id)). Gebruik dit patroon spaarzaam, complexe OR-voorwaarden zijn moeilijker te auditen en kunnen prestatie-implicaties hebben.
RLS-beleidsregels testen
Het testen van RLS is een van de belangrijkste en meest overgeslagen stappen in Supabase-ontwikkeling. Het doel is te verifiëren dat gebruikers precies de rijen zien die ze zouden moeten zien, en geen rijen zien die ze niet zouden moeten zien, met realistische testdata die elke beleidstak test.
De snelste manier om RLS te testen in Supabase is de Row Level Security Policies-tester in het Supabase-dashboard. Ga naar Authentication → Policies → klik op 'Test Policy' bij elk beleid. Voer de UUID van een gebruiker in en voer een query uit, je ziet precies wat die gebruiker zou ontvangen. Maak testgebruikers aan voor elke rol in je systeem (admin, member, viewer) en verifieer hun toegang tegen je testdata voordat je deployt.
Gebruik voor geautomatiseerd testen de JavaScript-client van Supabase met expliciete gebruikerssessies. Maak een testgebruiker aan, log deze in, en controleer vervolgens dat queries de verwachte rijen retourneren en dat schrijfacties naar niet-geautoriseerde rijen falen. Bewaar deze tests naast je applicatiecode en draai ze in CI. Wij schrijven doorgaans 5-10 RLS-tests per tabel die de gangbare gevallen dekken: eigen rijen zichtbaar, rijen van andere gebruikers onzichtbaar, correcte rol vereist voor verwijderen, insert geblokkeerd zonder juist eigendom.
Prestatieoverwegingen bij RLS
RLS-beleidsregels draaien bij elke query tegen de betreffende tabel. Een eenvoudige gelijkheidscontrole (auth.uid() = user_id) voegt verwaarloosbare overhead toe. Een subquery (workspace_members controleren bij elke rij) kan aanzienlijke latency toevoegen op schaal als deze niet correct is geïndexeerd.
De prestatieregel is: indexeer elke kolom die voorkomt in een WHERE-clausule van een beleidssubquery. Voor workspace-lidmaatschapscontroles heb je een index nodig op workspace_members(user_id) en idealiter een samengestelde index op workspace_members(user_id, workspace_id). Zonder deze voert PostgreSQL een sequentiële scan van workspace_members uit voor elke rij die je hoofdquery retourneert, bij 10K leden is dit onmerkbaar, bij 1M is het catastrofaal.
Een nuttige techniek om de overhead van beleidsevaluatie op veelgebruikte tabellen te verminderen, zijn security definer-functies. In plaats van een subquery in elk beleid in te bedden, maak je een PostgreSQL-functie die de lijst met workspace-ID's voor de huidige gebruiker retourneert, markeer je deze als SECURITY DEFINER en STABLE, en roep je haar aan vanuit je beleidsregels. PostgreSQL kan het resultaat van STABLE-functies cachen binnen één query-uitvoering, waardoor herhaalde subquery-evaluaties worden geëlimineerd. Dit kan de beleidsoverhead met 60-80% verminderen bij join-zware queries.