KoderKoder.ai
PrijzenEnterpriseOnderwijsVoor investeerders
InloggenAan de slag

Product

PrijzenEnterpriseVoor investeerders

Bronnen

Neem contact opOndersteuningOnderwijsBlog

Juridisch

PrivacybeleidGebruiksvoorwaardenBeveiligingBeleid voor acceptabel gebruikMisbruik melden

Sociaal

LinkedInTwitter
Koder.ai
Taal

© 2026 Koder.ai. Alle rechten voorbehouden.

Home›Blog›PostgreSQL row-level security voor SaaS: policies die werken
14 okt 2025·8 min

PostgreSQL row-level security voor SaaS: policies die werken

PostgreSQL row-level security voor SaaS helpt tenant-isolatie in de database afdwingen. Leer wanneer je het gebruikt, hoe je policies schrijft en wat je moet vermijden.

PostgreSQL row-level security voor SaaS: policies die werken

Het echte probleem dat RLS probeert op te lossen in SaaS-apps

In een SaaS-app is de meest gevaarlijke beveiligingsbug degene die opdook zodra je op schaal kwam. Je begint met een eenvoudige regel zoals “gebruikers mogen alleen de data van hun tenant zien”, vervolgens lanceer je snel een nieuw endpoint, voeg je een rapportagequery toe, of introduceer je een join die stilletjes de check overslaat.

Autoratie alleen in de applicatie faalt onder druk omdat de regels verspreid raken. De ene controller controleert tenant_id, een andere controleert membership, een background job vergeet het, en een “admin export” pad blijft maandenlang “tijdelijk”. Zelfs aandachtige teams missen wel eens een plek.

PostgreSQL row-level security (RLS) lost een concreet probleem op: het laat de database afdwingen welke rijen zichtbaar zijn voor een bepaald request. Het mentale model is simpel: elke SELECT, UPDATE en DELETE wordt automatisch gefilterd door policies, net zoals elk request gefilterd wordt door authenticatie-middleware.

Het woord “rijen” doet ertoe. RLS beschermt niet alles:

  • Het verbergt niet automatisch specifieke kolommen (gebruik views of kolomrechten).
  • Het maakt onveilige functies niet veilig (een functie kan nog steeds data lekken als die met hogere rechten draait).
  • Het valideert geen businessregels (bijvoorbeeld “alleen eigenaren mogen factureringsinstellingen wijzigen”).

Een concreet voorbeeld: je voegt een endpoint toe dat projecten opsomt met een join naar facturen voor een dashboard. Met alleen app-auth is het makkelijk om projects te filteren op tenant maar te vergeten invoices te filteren, of te joinen op een sleutel die tenants kruist. Met RLS kunnen beide tabellen tenant-isolatie afdwingen, zodat de query veilig faalt in plaats van data te lekken.

De afweging is reëel. Je schrijft minder herhaalde autorisatiecode en vermindert het aantal plekken dat kan lekken. Maar je krijgt ook nieuw werk: je moet policies zorgvuldig ontwerpen, vroeg testen, en accepteren dat een policy een query kan blokkeren die je had verwacht dat werkte.

Wanneer RLS autorisatie vereenvoudigt (en wanneer het pijn toevoegt)

RLS kan voelen als extra werk totdat je app groter wordt dan een handvol endpoints. Als je strikte tenant-grenzen hebt en veel query-paden (lijstschermen, zoeken, exports, admin-tools), betekent het neerleggen van de regel in de database dat je je niet overal dezelfde filter hoeft te herinneren.

RLS past goed wanneer de regel saai en universeel is: “een gebruiker mag alleen rijen van zijn tenant zien” of “een gebruiker mag alleen projecten zien waarvan hij lid is.” In die situaties verminderen policies fouten omdat elke SELECT, UPDATE en DELETE door dezelfde poort gaat, zelfs wanneer later een query wordt toegevoegd.

Het helpt ook in read-heavy apps waar de filterlogica consistent blijft. Als je API 15 verschillende manieren heeft om facturen te laden (op status, datum, klant, zoekopdracht), laat RLS je stoppen met het telkens opnieuw implementeren van tenant-filtering en kun je op de feature focussen.

RLS brengt pijn als de regels niet rijniveau-gebaseerd zijn. Per-veld regels zoals “je mag salaris zien maar niet bonus” of “maskeer deze kolom tenzij je HR bent” veranderen vaak in onhandige SQL en moeilijk onderhoudbare uitzonderingen.

Het is ook lastig voor zware rapportages die echt brede toegang nodig hebben. Teams maken dan vaak omgevingen of bypass-rollen voor “gewoon dit ene jobje”, en daar stapelen fouten zich op.

Voordat je commit: beslis of je de database de laatste poortwacht wilt laten zijn. Zo ja, plan voor discipline: test database-gedrag (niet alleen API-responses), behandel migraties als security-wijzigingen, vermijd snelle bypasses, bepaal hoe background jobs authenticeren en houd policies klein en herhaalbaar.

Als je tooling gebruikt die backends genereert, kan dat de levering versnellen, maar het neemt de noodzaak voor duidelijke rollen, tests en een eenvoudig tenantmodel niet weg. (Bijvoorbeeld: Koder.ai gebruikt Go en PostgreSQL voor gegenereerde backends, en je wilt RLS bewust ontwerpen in plaats van het “er later bij te strooien”.)

Datamodel basics die RLS-policies beheersbaar maken

RLS is het makkelijkst wanneer je schema al duidelijk aangeeft wie wat bezit. Als je begint met een vaag model en probeert dat “in policies te repareren”, krijg je meestal trage queries en verwarrende bugs.

Zet een tenant-sleutel waar die thuishoort

Kies één tenant-sleutel (zoals org_id) en gebruik die consequent. De meeste tenant-owned tabellen zouden die moeten hebben, zelfs als ze ook verwijzen naar een andere tabel die hem al heeft. Dit voorkomt joins binnen policies en houdt USING checks eenvoudig.

Een praktische regel: als een rij moet verdwijnen als een klant opzegt, heeft het waarschijnlijk org_id nodig.

Model memberships expliciet

RLS-policies beantwoorden meestal één vraag: “Is deze gebruiker lid van deze org, en wat mag hij doen?” Dat is moeilijk af te leiden uit ad-hoc kolommen.

Houd de kern-tabellen klein en eenvoudig:

  • users (één rij per persoon)
  • orgs (één rij per tenant)
  • org_memberships (user_id, org_id, role, status)
  • optioneel: project_memberships voor per-project toegang

Met dat in plaats kunnen je policies membership checken met één geindexeerde lookup.

Scheid gedeelde referentie-data van tenant-owned data

Niet alles heeft org_id nodig. Referentietabellen zoals landen, productcategorieën of plantypes zijn vaak gedeeld tussen alle tenants. Maak ze read-only voor de meeste rollen en koppel ze niet aan één org.

Tenant-owned data (projects, invoices, tickets) moet vermijden tenant-specifieke details via gedeelde tabellen naar binnen te halen. Houd gedeelde tabellen minimaal en stabiel.

FKs, cascades en indexering

Foreign keys werken nog steeds met RLS, maar deletes kunnen je verrassen als de deleter-rol afhankelijke rijen niet “kan zien”. Plan cascades zorgvuldig en test echte delete-flows.

Indexeer de kolommen waarop je policies filteren, vooral org_id en membership-sleutels. Een policy die leest als WHERE org_id = ... moet geen full-table scan worden wanneer de tabel miljoenen rijen bevat.

Hoe RLS-policies werken, zonder de enge details

RLS is een per-tabel schakel. Zodra het aanstaat, stopt PostgreSQL het vertrouwen in je app-code om de tenant-filter te onthouden. Elke SELECT, UPDATE en DELETE wordt gefilterd door policies, en elke INSERT en UPDATE wordt gevalideerd door policies.

De grootste mentale verschuiving: met RLS aan kunnen queries die vroeger data teruggaven, nu nul rijen teruggeven zonder foutmelding. Dat is PostgreSQL die toegang regelt.

Wat policies eigenlijk doen

Policies zijn kleine regels gekoppeld aan een tabel. Ze gebruiken twee checks:

  • USING is het read-filter. Als een rij niet voldoet aan USING, is die onzichtbaar voor SELECT, en kan die niet doelwit zijn van UPDATE of DELETE.
  • WITH CHECK is de write-poort. Het bepaalt welke nieuwe of gewijzigde rijen zijn toegestaan voor INSERT of UPDATE.

Een veelvoorkomend SaaS-patroon: USING zorgt dat je alleen rijen van je tenant ziet, en WITH CHECK zorgt dat je geen rij in iemand anders zijn tenant kunt invoegen door een tenant-id te raden.

PERMISSIVE vs RESTRICTIVE, in één zin

Als je later meer policies toevoegt, doet dit ertoe:

  • PERMISSIVE (standaard): een rij is toegestaan als enige policy hem toestaat.
  • RESTRICTIVE: een rij is toegestaan alleen als alle restrictieve policies hem toestaan (bovenop permissive gedrag).

Als je van plan bent regels te lagen zoals tenant-match plus rolchecks plus project-membership, kunnen restrictive policies intentie duidelijker maken, maar ze maken het ook makkelijker jezelf uit te sluiten als je één voorwaarde vergeet.

Hoe Postgres weet wie de gebruiker is

RLS heeft een betrouwbare “wie roept aan” waarde nodig. Gebruikelijke opties:

  • Een sessievariabele per request (bijvoorbeeld app.user_id en app.tenant_id).
  • JWT-claims gemapt naar session settings door je API-laag.
  • Role switching (SET ROLE ... per request), wat kan werken maar operationele overhead toevoegt.

Kies één aanpak en pas die overal toe. Het mixen van identity-bronnen over services is een snelle weg naar verwarrende bugs.

Policies benoemen zodat je later kunt debuggen

Gebruik een voorspelbare conventie zodat schema-dumps en logs leesbaar blijven. Bijvoorbeeld: {table}__{action}__{rule}, zoals projects__select__tenant_match.

Stap-voor-stap: voeg RLS toe aan één tabel en bewijs dat het werkt

Bezit je security-logica
Houd volledige controle met source export voor je RLS-helpers, tests en migraties.
Export Code

Als je nieuw bent met RLS, begin met één tabel en een klein bewijs. Het doel is niet perfecte dekking. Het doel is dat de database cross-tenant toegang weigert, zelfs als er een app-bug is.

Een kleine tabel om op te oefenen

Stel een eenvoudige projects tabel voor. Voeg eerst tenant_id toe op een manier die writes niet breekt.

ALTER TABLE projects ADD COLUMN tenant_id uuid;

-- Backfill existing rows (example: everyone belongs to a default tenant)
UPDATE projects SET tenant_id = '11111111-1111-1111-1111-111111111111'::uuid
WHERE tenant_id IS NULL;

ALTER TABLE projects ALTER COLUMN tenant_id SET NOT NULL;

Scheer daarna ownership en toegang van elkaar. Een gangbaar patroon is: één rol bezit tabellen (app_owner), een andere rol wordt door de API gebruikt (app_user). De API-rol zou niet de table owner moeten zijn, anders kan die policies omzeilen.

ALTER TABLE projects OWNER TO app_owner;
REVOKE ALL ON projects FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO app_user;

Bepaal nu hoe het request Postgres vertelt welke tenant het bedient. Eén eenvoudige aanpak is een request-gescopeerde setting. Je app zet die meteen nadat een transactie geopend is.

-- inside the same transaction as the request
SELECT set_config('app.current_tenant', '22222222-2222-2222-2222-222222222222', true);

Zet RLS aan en begin met lees-toegang.

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY projects_tenant_select
ON projects
FOR SELECT
TO app_user
USING (tenant_id = current_setting('app.current_tenant')::uuid);

Bewijs dat het werkt door twee verschillende tenants uit te proberen en te kijken of het aantal rijen verandert.

Voeg write-regels toe (WITH CHECK)

Read-policies beschermen writes niet. Voeg WITH CHECK toe zodat inserts en updates geen rijen in de verkeerde tenant smokkelen.

CREATE POLICY projects_tenant_write
ON projects
FOR INSERT, UPDATE
TO app_user
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);

Een snelle manier om gedrag (inclusief fouten) te verifiëren is een klein SQL-script dat je na elke migratie kunt herhalen:

  • BEGIN; SET LOCAL ROLE app_user;
  • SELECT set_config('app.current_tenant', '<tenant A>', true); SELECT count(*) FROM projects;
  • INSERT INTO projects(id, tenant_id, name) VALUES (gen_random_uuid(), '<tenant B>', 'bad'); (moet falen)
  • UPDATE projects SET tenant_id = '<tenant B>' WHERE ...; (moet falen)
  • ROLLBACK;

Als je dat script kunt draaien en elke keer dezelfde resultaten krijgt, heb je een betrouwbaar uitgangspunt voordat je RLS naar andere tabellen uitbreidt.

Policy-patronen die je in de meeste SaaS-apps hergebruikt

De meeste teams adopteren RLS als ze genoeg hebben van het steeds opnieuw herhalen van dezelfde autorisatiechecks in elke query. Het goede nieuws is dat de policy-vormen die je nodig hebt meestal consistent zijn.

Owner-rijen vs membership-rijen

Sommige tabellen zijn van nature door één gebruiker bezet (notes, API-tokens). Andere behoren toe aan een tenant waar toegang afhankelijk is van membership. Behandel deze als verschillende patronen.

Voor owner-only data controleren policies vaak created_by = app_user_id(). Voor tenant-data controleren policies vaak of de gebruiker een membership-rij voor de org heeft.

Een praktische manier om policies leesbaar te houden is identiteit te centraliseren in kleine SQL-helpers en die hergebruiken:

-- Example helpers
create function app_user_id() returns uuid
language sql stable as $$
  select current_setting('app.user_id', true)::uuid
$$;

create function app_is_admin() returns boolean
language sql stable as $$
  select current_setting('app.is_admin', true) = 'true'
$$;

Scheid leesregels van schrijflogica

Reads zijn vaak breder dan writes. Bijvoorbeeld: ieder org-lid kan SELECT projecten, maar alleen editors kunnen UPDATE, en alleen owners kunnen DELETE.

Maak het expliciet: één policy voor SELECT (membership), één policy voor INSERT/UPDATE met WITH CHECK (rol), en één voor DELETE (vaak strikter dan update).

Admin-override zonder RLS uit te schakelen

Vermijd “zet RLS uit voor admins.” Voeg liever een escape-hatch binnen policies toe, zoals app_is_admin(), zodat je niet per ongeluk volledige toegang geeft aan een gedeelde serviceroom.

Soft deletes en statusflags

Als je deleted_at of status gebruikt, verwerk dat in de SELECT policy (deleted_at is null). Anders kan iemand rijen “weer tot leven brengen” door flags te flippen waar de app van uitging dat ze definitief waren.

UPSERT: houd WITH CHECK vriendelijk

INSERT ... ON CONFLICT DO UPDATE moet voldoen aan WITH CHECK voor de rij ná de write. Als je policy vereist created_by = app_user_id(), zorg dat je upsert created_by zet bij insert en het niet overschrijft bij update.

Als je backend code genereert, zijn deze patronen de moeite waard om in interne templates te gieten zodat nieuwe tabellen met veilige defaults beginnen in plaats van een blanco vel.

Veelvoorkomende RLS-valkuilen die debugging lastig maken

RLS is geweldig totdat één klein detail het lijkt alsof PostgreSQL “willekeurig” data verbergt of toont. De fouten hieronder kosten de meeste tijd.

Valkuilen die stille tenant-lekken veroorzaken

De eerste val is het vergeten van WITH CHECK op insert en update. USING bepaalt wat je kunt zien, niet wat je mag creëren of wijzigen. Zonder WITH CHECK kan een app-bug een rij in de verkeerde tenant schrijven, en je merkt het misschien niet omdat diezelfde gebruiker die rij niet kan teruglezen.

Een andere veelvoorkomende lek is de “leaky join.” Je filtert projects correct, maar joinkt naar invoices, notes of files die niet op dezelfde manier beschermd zijn. De remedie is strikt maar eenvoudig: elke tabel die tenant-data kan onthullen heeft zijn eigen policy nodig, en views mogen niet alleen afhankelijk zijn van één veilige tabel.

Veelvoorkomende foutpatronen verschijnen vroeg:

  • Een read-policy bestaat, maar er ontbreekt WITH CHECK voor writes.
  • Een policy-conditie gebruikt een join naar een andere tabel die niet beschermd is.
  • Toegang wordt afgedwongen in een view, maar de onderliggende tabel is nog open.
  • Je vertrouwt op “de app zet altijd tenant_id”, en één background job vergeet het.
  • Je test met een superuser rol, dus je ziet nooit het echte gedrag.

Valkuilen die gedrag verwarrend maken

Policies die dezelfde tabel refereren (direct of via een view) kunnen recursie-surprises opleveren. Een policy kan membership checken door een view te queryen die de beschermde tabel weer leest, wat leidt tot fouten, trage queries of een policy die nooit matcht.

Role-setup is een andere bron van verwarring. Table owners en verhoogde rollen kunnen RLS omzeilen, dus je tests slagen terwijl echte gebruikers falen (of andersom). Test altijd met dezelfde laag-rechten rol die je app ook gebruikt.

Wees voorzichtig met SECURITY DEFINER functies. Ze draaien met de privileges van de functeeigenaar, dus een helper zoals current_tenant_id() kan prima zijn, maar een “handige” functie die data leest kan per ongeluk over tenants heen lezen tenzij je hem zo ontwerpt dat hij RLS respecteert.

Stel ook een veilige search_path in binnen security definer-functies. Als je dat niet doet, kan de functie een ander object met dezelfde naam oppikken, en je policy-logica kan stilletjes naar het verkeerde object wijzen afhankelijk van de sessiestatus.

RLS debuggen: praktische manieren om te zien wat er gebeurt

Voorkom leaky joins vroeg
Maak een rapportagescherm en verifieer dat joins tenant-veilig blijven met RLS.
Bouw Dashboard

RLS-bugs komen meestal door ontbrekende context, niet door “slechte SQL.” Een policy kan op papier correct zijn en toch falen omdat de session role anders is dan je denkt, of omdat het request nooit de tenant- en gebruikerwaarden zette waarop de policy vertrouwt.

Een betrouwbare manier om een productie-rapport te reproduceren is dezelfde sessie-setup lokaal te spiegelen en de exacte query uit te voeren. Dat betekent meestal:

  • SET ROLE app_user; (of de echte API-rol)
  • SELECT set_config('app.tenant_id', 't_123', true); en SELECT set_config('app.user_id', 'u_456', true);
  • Voer exact dezelfde SQL uit die je app draaide (inclusief parameters)
  • Bevestig wat Postgres ziet: SELECT current_user, current_setting('app.tenant_id', true), current_setting('app.user_id', true);

Als je niet zeker weet welke policy toegepast wordt, kijk dan in de catalog in plaats van te raden. pg_policies toont elke policy, het commando en de USING en WITH CHECK expressies. Koppel dat aan pg_class om te bevestigen dat RLS op de tabel aanstaat en niet omzeild wordt.

Prestatieproblemen kunnen lijken op auth-problemen. Een policy die een membership-tabel joint of een functie aanroept kan correct maar traag zijn zodra de tabel groeit. Gebruik EXPLAIN (ANALYZE, BUFFERS) op de gereproduceerde query en kijk naar sequentiële scans, onverwachte nested loops of filters die laat worden toegepast. Ontbrekende indexes op (tenant_id, user_id) en membership-tabellen zijn veelvoorkomende oorzaken.

Het helpt ook om drie waarden per request te loggen op applicatieniveau: de tenant ID, de user ID en de database-rol gebruikt voor het request. Als die niet overeenkomen met wat je dacht te hebben gezet, zal RLS zich “verkeerd” gedragen omdat de inputs verkeerd zijn.

Voor tests, houd een paar seed-tenants en maak failures expliciet. Een kleine suite bevat meestal: “Tenant A kan Tenant B niet lezen,” “gebruiker zonder membership kan het project niet zien,” “owner kan updaten, viewer niet,” “insert wordt geblokkeerd tenzij tenant_id overeenkomt met context,” en “admin override geldt alleen waar bedoeld.”

Snelle pre-release checklist voor RLS in productie

Behandel RLS als een veiligheidsriem, niet als een feature-toggle. Kleine missers veranderen in “iedereen ziet ieders data” of “alles geeft nul rijen terug.”

Datamodel en policy-vorm

Zorg dat je tabelontwerp en policy-regels bij je tenant-model passen.

  • Elke tenant-owned tabel moet een duidelijke tenant-sleutel hebben (meestal tenant_id). Als die ontbreekt, schrijf op waarom (bijv. globale referentietabellen).
  • Schakel RLS in op elke tenant-owned tabel, niet alleen de “hoofd” tabellen. Als sommige paden nooit mogen omzeilen, overweeg FORCE ROW LEVEL SECURITY op die tabellen.
  • Splits read- en write-regels. Reads gebruiken USING. Writes moeten WITH CHECK hebben zodat inserts en updates een rij niet in een andere tenant kunnen plaatsen.
  • Houd policy-predicaten indexvriendelijk. Als policies filteren op tenant_id of joinen via membership-tabellen, voeg de bijbehorende indexes toe.

Een simpele sanity-scenario: een tenant A gebruiker kan zijn eigen facturen lezen, kan alleen een factuur invoegen voor tenant A, en kan een factuur niet updaten om tenant_id te veranderen.

Rollen, performance en tests

RLS is zo sterk als de rollen die je app gebruikt.

  • Bevestig dat de app nooit als superuser, table owner of enige rol met bypassrls connecteert.
  • Draai een paar echte queries met productieachtige datavolumes en controleer queryplannen.
  • Voeg geautomatiseerde negatieve tests toe die bewijzen dat cross-tenant toegang faalt.

Voorbeeld: multi-tenant projects-app met membership-gebaseerde toegang

Ontwerp beleid vóór coderen
Plan je rollen, sessie-instellingen en beleidsregels voordat je code genereert.
Gebruik Planning

Stel je een B2B-app voor waar bedrijven (orgs) projecten hebben, en projecten taken. Gebruikers kunnen bij meerdere orgs horen, en een gebruiker kan lid zijn van sommige projecten maar niet van andere. Dit is een goede match voor RLS omdat de database tenant-isolatie kan afdwingen, zelfs als een API-endpoint een filter vergeet.

Een eenvoudig model is: orgs, users, org_memberships (org_id, user_id, role), projects (id, org_id), project_memberships (project_id, user_id), tasks (id, project_id, org_id, ...). Die org_id op tasks is intentioneel. Het houdt policies simpel en vermindert verrassingen tijdens joins.

Een klassiek lek gebeurt wanneer tasks alleen project_id hebben, en je policy toegang controleert via een join naar projects. Eén fout (een permissive policy op projects, een join die een conditie weglaat, of een view die context verandert) kan taken van een andere org blootstellen.

Een veiligere migratiepad vermijdt het breken van productieverkeer:

  • Ship eerst schema-wijzigingen (voeg org_id toe aan tasks, voeg membership-tabellen toe).
  • Backfill tasks.org_id uit projects.org_id, en voeg daarna NOT NULL toe.
  • Voeg policies toe maar houd RLS uitgeschakeld terwijl je in staging test.
  • Schakel RLS aan, forceer het, en verwijder pas daarna oude app-side filters.

Support-toegang kun je het beste afhandelen met een smalle break-glass rol, niet door RLS uit te schakelen. Houd die gescheiden van normale support-accounts en maak expliciet wanneer hij gebruikt wordt.

Documenteer de regels zodat policies niet verschuiven: welke session-variabelen moeten gezet worden (user_id, org_id), welke tabellen org_id moeten dragen, wat “member” betekent, en een paar SQL-voorbeelden die 0 rijen zouden moeten teruggeven als ze als de verkeerde org uitgevoerd worden.

Volgende stappen: rol RLS veilig uit en houd het onderhoudbaar

RLS is het makkelijkst als je het als een productwijziging behandelt. Rol het uit in kleine stukken, bewijs gedrag met tests en houd een duidelijk verslag waarom elke policy bestaat.

Een rollout-plan dat vaak werkt:

  • Begin met één tabel die duidelijke tenant-eigendom heeft (bijv. projects) en sluit die af.
  • Voeg tests toe die toegestane en geblokkeerde reads en writes dekken voor een paar rollen (owner, member, outsider).
  • Breid uit in batches (één featuregebied tegelijk) die je in één sessie kunt debuggen.
  • Monitor permissiefouten tijdens uitrol en deploy in een laag-risico venster.

Nadat de eerste tabel stabiel is, maak policy-wijzigingen doelbewust. Voeg een policy-review stap toe aan migraties en voeg een korte intentie-notitie toe (wie mag wat en waarom) plus een bijpassende test-update. Dit voorkomt “voeg gewoon nog een OR toe” policies die langzaam in een lek veranderen.

Als je snel beweegt, kunnen tools zoals Koder.ai (koder.ai) je helpen een Go + PostgreSQL startpunt te genereren via chat, en dan kun je RLS-policies en tests met dezelfde discipline bovenop leggen als bij een handmatig opgebouwde backend.

Tot slot: houd veiligheidsvoorzieningen tijdens rollout. Maak snapshots vóór policy-migraties, oefen rollback totdat het saai wordt, en houd een kleine break-glass route voor support die RLS niet over het hele systeem uitschakelt.

Veelgestelde vragen

What security problem does RLS actually solve in a SaaS app?

RLS laat PostgreSQL afdwingen welke rijen zichtbaar of schrijfbaar zijn voor een request, zodat tenant-isolatie niet afhangt van dat elke endpoint de juiste WHERE tenant_id = ... filter onthoudt. Het grootste voordeel is dat je minder kwetsbaar bent voor “één vergeten check” fouten wanneer je app groeit en queries toenemen.

When is RLS worth the extra complexity?

Het is de moeite waard wanneer toegangsregels consistent en rijniveau-gericht zijn, zoals tenant-isolatie of toegangscontrole op basis van membership, en je veel verschillende query-paden hebt (zoeken, exports, adminschermen, background jobs). Het is meestal minder zinvol als de regels vooral per-veld zijn, veel uitzonderingen kennen of wanneer rapportages breed over tenants moeten lezen.

What does RLS NOT protect me from?

RLS zorgt voor rijniveau zichtbaarheid en basis write-gating. Kolomprivacy vraagt vaak om views en kolomrechten, en complexe businessregels (zoals facturerings-eigendom of goedkeuringsstromen) horen meestal thuis in applicatielogica of in zorgvuldig ontworpen databaseconstraints.

What’s the safest way to start using RLS if I’m new to it?

Maak een laag-privilege rol voor de API (niet de table owner), zet RLS aan, voeg dan een SELECT policy en een INSERT/UPDATE policy met WITH CHECK toe. Gebruik een request-gescopeerde sessievariabele (zoals app.current_tenant) en verifieer dat het wisselen daarvan verandert welke rijen je kunt zien en schrijven.

How should my app tell Postgres which tenant and user is making the request?

Een veelgebruikte manier is een sessievariabele per request, ingesteld aan het begin van de transactie, zoals app.tenant_id en app.user_id. Belangrijk is consistentie: elk codepad (web-requests, jobs, scripts) moet dezelfde waarden zetten waar policies op vertrouwen, anders krijg je verwarrend “zero rows” gedrag.

What’s the difference between USING and WITH CHECK in an RLS policy?

USING bepaalt welke bestaande rijen zichtbaar en targetable zijn voor SELECT, UPDATE en DELETE. WITH CHECK bepaalt welke nieuwe of gewijzigde rijen toegestaan zijn tijdens INSERT en UPDATE, zodat je voorkomt dat er per ongeluk in een andere tenant geschreven wordt.

Why do people keep saying “don’t forget WITH CHECK”?

Als je alleen USING toevoegt, kan een bug in een endpoint alsnog rijen schrijven in de verkeerde tenant en je merkt het misschien niet omdat dezelfde gebruiker die rij niet terug kan lezen. Koppel altijd een tenant-readregel aan een bijpassende WITH CHECK regel voor writes zodat slechte data niet gemaakt kan worden.

How should I structure my schema so RLS policies stay simple and fast?

Vermijd joins binnen policies door de tenant-sleutel (zoals org_id) direct op tenant-gebruikte tabellen te zetten, zelfs als ze ook naar een andere tabel verwijzen die die sleutel heeft. Voeg expliciete membership-tabellen toe (org_memberships, optioneel project_memberships) zodat policies met één geindexeerde lookup kunnen werken in plaats van ingewikkelde afleidingen.

How do I debug “RLS is hiding my data” without guessing?

Reproduceer eerst dezelfde sessiecontext die je app gebruikt door dezelfde rol en sessie-instellingen te zetten en voer dan exact de SQL uit. Controleer daarna of RLS aanstaat en inspecteer pg_policies om te zien welke USING en WITH CHECK expressies van toepassing zijn — RLS faalt vaak omdat de identiteitcontext ontbreekt, niet door “slechte SQL.”

If I generate my backend (for example with Koder.ai), do I still need to design RLS carefully?

Ja, maar beschouw gegenereerde code als uitgangspunt, niet als complete beveiliging. Als je Koder.ai gebruikt om een Go + PostgreSQL backend te genereren, moet je nog steeds je tenant-model definiëren, sessie-identiteit consistent zetten en policies en tests bewust toevoegen zodat nieuwe tabellen niet zonder juiste bescherming worden opgeleverd.

Inhoud
Het echte probleem dat RLS probeert op te lossen in SaaS-appsWanneer RLS autorisatie vereenvoudigt (en wanneer het pijn toevoegt)Datamodel basics die RLS-policies beheersbaar makenHoe RLS-policies werken, zonder de enge detailsStap-voor-stap: voeg RLS toe aan één tabel en bewijs dat het werktPolicy-patronen die je in de meeste SaaS-apps hergebruiktVeelvoorkomende RLS-valkuilen die debugging lastig makenRLS debuggen: praktische manieren om te zien wat er gebeurtSnelle pre-release checklist voor RLS in productieVoorbeeld: multi-tenant projects-app met membership-gebaseerde toegangVolgende stappen: rol RLS veilig uit en houd het onderhoudbaarVeelgestelde vragen
Delen
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo