TL;DR
Nedan finns de RLS-policyer som de flesta Supabase-appar behöver, var och en körd genom REST-API:t som gäst, som radens ägare och som en annan inloggad användare. Tre resultat är värda att känna till innan du kopierar något: en uppdatering av någon annans rad ger 200 med en tom lista, inte ett fel; en harmlöst utseende insert-policy på en medlemskapstabell lät en användare gå med i ett annat företag och läsa dess projekt; och att skriva (select auth.uid()) i stället för auth.uid() tog en fråga på 200 000 rader från 156 ms till 7,4 ms.
Jag heter Denys och är full-stack- och AI-utvecklare, och de flesta appar jag bygger eller reparerar körs på Supabase. Exempel på en Supabase RLS policy finns överallt; vad de gör när en riktig förfrågan träffar dem visas sällan. Så jag körde dem.
Så testade jag. De flesta förfrågningar gick genom en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) med publishable key och två riktiga inloggade användare, A och B. Fall som inte kräver HTTP, som syntaxfel och tidsmätningar, kördes på en vanlig lokal Postgres 17.10 med Supabases roller anon och authenticated och återskapad auth.uid(). Allt den 24 oktober 2026, med påhittade rader.
Före varje policy: har tabellen ett grant?
En policy avgör vilka rader en roll ser. Ett grant avgör om rollen alls når tabellen, och på aktuella Supabase-projekt får nya tabeller inte längre automatiskt Data API-grants (från och med den 30 oktober 2026 för alla projekt). På min stack svarade en tabell med RLS på och utan grant så här:
| Förfrågan | Status | Body |
|---|---|---|
Gäst GET /invoices |
401 |
permission denied for table invoices, med tipset GRANT SELECT ... TO anon |
Användare A GET /invoices |
403 |
samma, med tipset för authenticated |
Så steg ett för varje tabell som appen läser från webbläsaren:
alter table public.todos enable row level security;
grant select, insert, update, delete on public.todos to authenticated;
Ge bara de kommandon webbläsaren behöver, och bara till authenticated, om inte gäster ska läsa den.
Vilken Supabase RLS policy låter användare bara se sina egna rader?
En policy per kommando, var och en begränsad till inloggade användare:
create policy "read own" on public.todos
for select to authenticated using ((select auth.uid()) = user_id);
create policy "add own" on public.todos
for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "edit own" on public.todos
for update to authenticated
using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "delete own" on public.todos
for delete to authenticated using ((select auth.uid()) = user_id);
A äger två rader, B äger en. Det som kom tillbaka:
| Förfrågan från | Vad | Status | Body |
|---|---|---|---|
| Gäst | läs allt | 401 |
permission denied (inget grant för anon) |
| A | läs allt | 200 |
A:s 2 rader |
| A | infoga en rad som ägs av B | 403 |
new row violates row-level security policy |
| A | uppdatera B:s rad | 200 |
[] |
| A | ta bort B:s rad | 200 |
[] |
| A | byt ägare på egen rad till B | 403 |
new row violates row-level security policy |
De två 200 []-raderna är de att minnas. RLS avvisar inte en uppdatering eller borttagning av rader du inte kan se; den filtrerar bort dem, och noll ändrade rader räknas som lyckat. Om din app tar “inget fel” som “sparat” säger den till användaren att ändringen gick igenom när ingenting hände. Be om de ändrade raderna (.select() i supabase-js, Prefer: return=representation över HTTP) och kontrollera att en kom tillbaka.
Varför behöver en update-policy WITH CHECK?
using väljer vilka befintliga rader en användare får röra; with check avgör hur de raderna får se ut efteråt. Jag jämförde tre versioner av samma update-policy:
| Update-policy | A ger egen rad till B |
|---|---|
using (uid = user_id) with check (uid = user_id) |
403, nekad |
using (uid = user_id), utan with check |
nekad (Postgres återanvänder using som kontroll) |
using (uid = user_id) with check (true) |
200, raden tillhör nu B |
Att utelämna with check är säkrare än det ser ut, eftersom Postgres faller tillbaka på using-uttrycket. with check (true), som vissa generatorer skriver för att “tillåta uppdateringar”, är det som låter en användare flytta en rad till någon annans konto. Skriv ut kontrollen i sin helhet.
Hur gör du profiler publika men bara redigerbara av ägaren?
create policy "public read" on public.profiles
for select to anon, authenticated using (true);
create policy "own edit" on public.profiles
for update to authenticated
using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
I körningen på vanlig Postgres: gästen läste båda profilerna, gästens uppdatering ändrade 0 rader, och A:s update profiles set display_name = 'x' utan filter ändrade exakt 1 rad, A:s egen. using (true) är fint för ett select på data som är avsedda att vara publika. På vilken annan tabell som helst är det detsamma som ingen RLS, och en policy skriven utan to-klausul gäller även gäster: min gäst läste den “privata” raden genom en using (true)-policy utan to.
Hur delar du rader inom ett team?
Den vanliga formen: en memberships-tabell och en policy på datan som slår upp en användares team i den.
create policy "members read" on public.projects
for select to authenticated
using (org_id in (select org_id from public.memberships
where user_id = (select auth.uid())));
Den policyn är fin. Läckan fanns bredvid. Jag gav memberships den policy som många guider föreslår, “användare kan infoga sitt eget medlemskap”:
create policy "join any org" on public.memberships
for insert to authenticated with check (user_id = (select auth.uid()));
Den kontrollerar vem raden är för, inte vilket team. A, medlem i företag 1, skickade en förfrågan:
| Förfrågan från A | Status | Body |
|---|---|---|
| läs projekt | 200 |
A roadmap |
infoga medlemskap {user_id: A, org_id: 2} |
201 |
skapat |
| läs projekt igen | 200 |
A roadmap, B payroll |
Åtgärden är att inte ge memberships någon insert-policy alls, så att webbläsaren inte kan skriva till den (i Postgres-körningen nekades samma insert då med 42501), och att lägga till medlemmar via ett inbjudningsflöde som körs på servern eller i en funktion som kontrollerar inbjudan. En användare som tillhör två företag är nästa fälla; den skrev jag om i Supabase RLS: användare i två organisationer läcker.
Vad om en policy behöver läsa sin egen tabell?
“Medlemmar kan se sina lagkamrater” betyder en policy på memberships som läser memberships. Postgres vägrar vid frågetillfället:
ERROR 42P17: infinite recursion detected in policy for relation "members2"
Flytta uppslaget till en funktion som körs som sin ägare, i ett schema som API:t inte exponerar, med fast search_path:
create schema private;
create function private.user_org_ids() returns setof int
language sql stable security definer set search_path = ''
as $$ select org_id from public.members2 where user_id = (select auth.uid()) $$;
revoke all on function private.user_org_ids() from public;
grant usage on schema private to authenticated;
grant execute on function private.user_org_ids() to authenticated;
create policy "see teammates" on public.members2
for select to authenticated using (org_id in (select private.user_org_ids()));
Efter det såg A båda medlemmarna i företag 1 och B såg bara sig själv. Håll funktionen utanför public: allt där går att anropa via API:t.
Hur kräver du MFA för känsliga rader?
Lägg till en restriktiv policy ovanpå den vanliga. Restriktiva policyer kombineras med and, så båda måste godkänna:
create policy "mfa only" on public.payouts as restrictive
for select to authenticated using ((select auth.jwt() ->> 'aal') = 'aal2');
Med samma användare läste en session på aal1 0 utbetalningar och en session på aal2 läste 1.
Vilka tabeller ska ha RLS på och ingen policy?
Tabeller som bara din server rör. RLS på utan policy nekar webbläsarrollerna allt: i Postgres-körningen läste gästen och A båda 0 rader, och A:s insert nekades med 42501. Att lämna RLS av är raka motsatsen. En tabell med RLS av och standardgrants, läst med publishable key och utan inloggning:
| Förfrågan från gäst | Status | Body |
|---|---|---|
GET /leads |
200 |
båda raderna |
DELETE /leads?id=gt.0 |
200 |
båda raderna, borttagna |
Det är vad Advisor-varningen “RLS disabled in public” handlar om.
Kan en policy täcka flera kommandon?
Inte med en lista. for update, delete är ett syntaxfel (42601: syntax error at or near ","), även om det fortfarande finns i publicerade samlingar av exempel. for all är giltigt och tillämpar ett using och ett with check på varje kommando, vilket sällan är vad man vill. En insert-policy med using misslyckas också: only WITH CHECK expression allowed for INSERT.
Gör policyer frågor långsammare?
Det kan de. Ett policy-uttryck körs för varje rad Postgres överväger. Jag tidsmätte select * from big som användare A på 200 000 rader som ägs av 1 000 användare, där A äger 200 (median av 7 körningar, Postgres 17.10):
| Policy och index | Mediantid |
|---|---|
auth.uid() = user_id, utan index |
156,3 ms |
(select auth.uid()) = user_id, utan index |
7,4 ms |
auth.uid() = user_id, index på user_id |
0,21 ms |
(select auth.uid()) = user_id, index |
0,05 ms |
Att linda in anropet i select låter Postgres beräkna det en gång per fråga i stället för en gång per rad; indexet gör resten. Supabases guide om RLS-prestanda rapporterar samma storleksordning på sin egen tabell med 100 000 rader (179 ms till 9 ms). Många blogginlägg citerar de siffrorna som sina egna; siffrorna ovan är från min körning.
Varifrån ska roller komma?
Från app_metadata eller din egen tabell, aldrig från user_metadata. En inloggad användare kan ändra sin egen user_metadata med supabase.auth.updateUser(), så en policy som kontrollerar auth.jwt() -> 'user_metadata' ->> 'role' låter dem ge sig själva rollen. En registreringstrigger som kopierar en roll från registreringens metadata gör detsamma; det fallet finns i Supabase raw_user_meta_data: så blev en registrering Owner.
Hur kontrollerar du dina egna policyer?
- Den kostnadsfria Supabase-säkerhetskontrollen läser ditt projekt skrivskyddat och flaggar policyer som alltid är sanna, skrivpolicyer för gäster, policyer som litar på användarmetadata och tabeller med RLS av, med åtgärds-SQL för var och en.
- Ingen skanner vet vilka rader som tillhör vem. För det kör du förfrågningar som två användare, som den här artikeln gör; den fullständiga metoden för en app med roller finns i hur du testar Supabase RLS.
- Vad Supabase säkrar åt dig och vad det lämnar åt dig finns i Är Supabase säkert?.
- De samma ägar- och teamtesterna, körda även på Appwrite, finns i Appwrite vs Supabase säkerhet: samma attacker på båda.
Begränsningar. En lokal stack och en bärbar dator, påhittad data, 24 oktober 2026. Tider beror på hårdvara och tabellens form; förhållandena betyder mer än millisekunderna. Storage-policyer och Realtime ingår inte här.
Osäker på om dina policyer håller?
Jag tittar gärna på ett schema tillsammans med dig. Ingen säljpitch.
Hör av dig