Varför RLS är viktigt för SaaS
Utan RLS är ditt API den enda barriären mellan en användare och varje rad i din databas. En oavsiktlig "hämta alla"-fråga och du har ett dataintrång.
Med RLS upprätthåller PostgreSQL åtkomstregler vid frågekörningstid. Oavsett vad ditt API skickar returnerar databasen bara rader som den autentiserade användaren får se. Detta är säkerhet-på-djupet, ditt API och din databas upprätthåller åtkomstkontroll oberoende av varandra.
Ur ett GDPR-perspektiv är detta guld: du kan visa för dina kunder (och deras DPO:er) att dataisolering upprätthålls på databasnivå, den starkaste möjliga tekniska garantin. Vid ett eventuellt dataskyddstillsynsärende är detta ett starkt bevis på privacy-by-design.
Aktivera RLS
Aktivera RLS på varje användarvändig tabell. Tabeller utan RLS-policyer är vidöppna som standard (om de nås via service-rollnyckeln) eller helt otillgängliga (om de nås via anon-nyckeln utan policyer).
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
När det är aktiverat returneras inga rader som standard tills du skapar policyer. Detta är den korrekta säkerhetspositionen.
Skapa en checklista för ditt team: varje ny tabell får RLS aktiverat innan den ansluts till frontenden. Vi rekommenderar att lägga till detta som ett steg i er PR-mall eller deploy-checklista.
Den grundläggande användarägarskaps-policyn
Det vanligaste mönstret, användare kan bara se och ändra sina egna rader: 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);
Dessa fyra policyer täcker 80 % av alla enskilda användarappar. För B2C-appar, konsument-SaaS, personliga verktyg, individuella dashboards, är detta din fullständiga RLS-implementering.
Multi-tenant workspace-policyer
För SaaS med team och arbetsplatser kontrollerar du workspace-medlemskap:
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()
)
);
Detta kontrollerar workspace_members-kopplingstabell vid varje fråga. Indexera workspace_members(user_id, workspace_id) för prestanda.
För svenska B2B SaaS-bolag med enterprise-kunder: lägg till en organisationshierarki (t.ex. koncern → dotterbolag → avdelning) som ytterligare RLS-dimension. Det möjliggör koncernrapportering med korrekt dataisolering per enhet.
Rollbaserad åtkomstkontroll
Lägg till rollkontroll för adminoperationer:
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'
)
);
Lagra roller i din workspace_members-tabell (role text DEFAULT 'member'). Roller: owner, admin, member, viewer.
För mer granulär kontroll kan du bygga ett behörighetssystem (permissions-tabell) och kontrollera specifika åtgärder istället för roller. Men börja enkelt: owner/admin/member täcker 90 % av B2B SaaS-behoven.
Vanliga misstag att undvika
1. Glömma att aktivera RLS på nya tabeller. Skapa en checklista: varje ny tabell får RLS aktiverat innan den ansluts till frontenden.
2. Använda service-rollnyckeln i frontenden. Service-rollnyckeln kringgår RLS. Exponera den aldrig för användare, använd anon-nyckeln i WeWeb/FlutterFlow och service-rollen endast i serversidiga Xano-funktioner.
3. Ingen policy för INSERT. Många utvecklare lägger till SELECT- och UPDATE-policyer men glömmer INSERT. Utan det kan anon-nyckelsanvändare inte skapa rader alls.
4. N+1-policysökningar. Om din policy JOINar en stor tabell vid varje fråga ser du prestandaproblem i skala. Materialisera medlemskapssökningar eller använd index aggressivt. Testa alltid dina RLS-policyers prestanda med representativ datavolym innan du lanserar.
Skriva RLS-policyer från grunden
Varje RLS-policy har tre komponenter: tabellen den gäller för, operationen den styr (SELECT, INSERT, UPDATE, DELETE eller ALL), och uttrycket som utvärderas till sant eller falskt för varje rad. USING-satsen gäller rader som läses eller ändras, den kör en kontroll mot befintliga rader. WITH CHECK-satsen gäller bara för INSERT och UPDATE, den validerar den nya raden som skrivs.
En vanlig källa till förvirring: du behöver separata USING- och WITH CHECK-uttryck för UPDATE-policyer. USING styr vilka rader användaren kan uppdatera (vilka rader de kan hitta), och WITH CHECK styr vilka värden de kan skriva in i de raderna. Om du vill att användare ska kunna uppdatera sina egna rader men inte ändra fältet user_id, behöver din UPDATE-policy: USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id). Utan WITH CHECK skulle en användare kunna uppdatera en rad de äger och sätta user_id till en annan användares ID, vilket i praktiken överför ägarskapet.
För komplexa policyer som involverar subqueries (workspace-medlemskapskontroller, behörighetstabellsökningar), skriv ut subqueryn först och testa den som en fristående SELECT innan du bäddar in den i en policy. PostgreSQLs EXPLAIN ANALYZE är din vän här, kör det på ett policyuttryck för att se om det använder index eller faller tillbaka på en sekventiell skanning.
Vanliga RLS-mönster
Mönster 1: Användaren äger raden. Det enklaste och vanligaste. user_id-kolumnen refererar till auth.users. USING (auth.uid() = user_id). Fungerar för anteckningar, dokument, personliga inställningar.
Mönster 2: Team- eller workspace-åtkomst. En workspace_members-kopplingstabell avgör åtkomst. USING (workspace_id IN (SELECT workspace_id FROM workspace_members WHERE user_id = auth.uid())). Fungerar för all delad-resurs-SaaS.
Mönster 3: Offentlig läsning, ägarskrivning. Innehållsplattformar där vem som helst kan läsa men bara författaren kan redigera. SELECT-policy: USING (true) eller USING (published = true). UPDATE/DELETE-policy: USING (auth.uid() = author_id). Fungerar för bloggar, kataloger, marknadsplatser.
Mönster 4: Admin-bypass. Administratörer kan se alla rader i en workspace oavsett andra ägarskapsregler. Lägg till en OR-sats: USING (auth.uid() = user_id OR auth.uid() IN (SELECT user_id FROM workspace_members WHERE role = 'admin' AND workspace_id = projects.workspace_id)). Använd det här mönstret sparsamt, komplexa OR-villkor är svårare att granska och kan ha prestandaimplikationer.
Testa RLS-policyer
Att testa RLS är ett av de viktigaste och mest överhoppade stegen i Supabase-utveckling. Målet är att verifiera att användare kan se exakt de rader de ska se, och inte kan se rader de inte ska, med hjälp av realistisk testdata som övar varje policygren.
Det snabbaste sättet att testa RLS i Supabase är Row Level Security Policies-testaren i Supabase-instrumentpanelen. Gå till Authentication → Policies → klicka på 'Test Policy' på valfri policy. Ange en användares UUID och kör en fråga, du ser exakt vad den användaren skulle få. Skapa testanvändare för varje roll i ditt system (admin, member, viewer) och verifiera deras åtkomst mot din testdata innan du lanserar.
För automatiserad testning, använd Supabases JavaScript-klient med explicita användarsessioner. Skapa en testanvändare, logga in dem, verifiera sedan att frågor returnerar förväntade rader och att skrivningar till obehöriga rader misslyckas. Spara dessa tester tillsammans med din applikationskod och kör dem i CI. Vi skriver vanligtvis 5-10 RLS-tester per tabell som täcker de vanliga fallen: egna rader synliga, andra användares rader osynliga, korrekt roll krävs för borttagning, insättning blockerad utan korrekt ägarskap.
Prestandaöverväganden för RLS
RLS-policyer körs vid varje fråga mot den berörda tabellen. En enkel likhetskontroll (auth.uid() = user_id) lägger till försumbar overhead. En subquery (som kontrollerar workspace_members för varje rad) kan lägga till betydande latens i skala om den inte indexeras korrekt.
Prestandaregeln är: indexera varje kolumn som förekommer i en policy-subquerys WHERE-sats. För workspace-medlemskapskontroller behöver du ett index på workspace_members(user_id) och helst ett sammansatt index på workspace_members(user_id, workspace_id). Utan dessa utför PostgreSQL en sekventiell skanning av workspace_members för varje rad som returneras av din huvudfråga, vid 10 000 medlemmar är detta omärkbart, vid 1 miljon är det katastrofalt.
En användbar teknik för att minska overheaden av policyutvärdering på hett använda tabeller är security definer-funktioner. Istället för att bädda in en subquery i varje policy, skapa en PostgreSQL-funktion som returnerar listan av workspace-ID:n för den aktuella användaren, markera den som SECURITY DEFINER och STABLE, och anropa den från dina policyer. PostgreSQL kan cacha resultatet av STABLE-funktioner inom en enskild frågekörning, vilket eliminerar upprepade subquery-utvärderingar. Detta kan minska policy-overheaden med 60-80 % på join-tunga frågor.