Gå til indhold

Supabase RLS policy-eksempler, hver testet via API

Vis som MarkdownÅbn i ClaudeÅbn i ChatGPTÅbn i Perplexity

Spørgsmålet kopieres også. Hvis chatten åbner tom, så indsæt det.

TL;DR

Nedenfor står de RLS-policies, som de fleste Supabase-apps har brug for, hver kørt gennem REST-API’et som gæst, som rækkens ejer og som en anden logget ind bruger. Tre resultater er værd at kende, før du kopierer noget: en opdatering af en andens række giver 200 med en tom liste, ikke en fejl; en uskyldigt udseende insert-policy på en medlemskabstabel lod en bruger blive medlem af et andet firma og læse dets projekter; og (select auth.uid()) i stedet for auth.uid() bragte en forespørgsel på 200.000 rækker fra 156 ms til 7,4 ms.

Jeg hedder Denys og er full-stack- og AI-udvikler, og de fleste apps, jeg bygger eller reparerer, kører på Supabase. Eksempler på en Supabase RLS policy findes overalt; hvad de gør, når en rigtig forespørgsel rammer dem, bliver sjældent vist. Så jeg kørte dem.

Sådan testede jeg. De fleste forespørgsler gik gennem en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) med publishable key og to rigtige loggede brugere, A og B. Tilfælde, der ikke kræver HTTP, som syntaksfejl og tidsmålinger, kørte på en almindelig lokal Postgres 17.10 med Supabases roller anon og authenticated og genskabt auth.uid(). Alt den 24. oktober 2026, med opfundne rækker.

Før enhver policy: har tabellen et grant?

En policy afgør, hvilke rækker en rolle ser. Et grant afgør, om rollen overhovedet når tabellen, og på nuværende Supabase-projekter får nye tabeller ikke længere automatisk Data API-grants (fra den 30. oktober 2026 for alle projekter). På min stack svarede en tabel med RLS slået til og uden grant sådan:

Forespørgsel Status Body
Gæst GET /invoices 401 permission denied for table invoices, med hintet GRANT SELECT ... TO anon
Bruger A GET /invoices 403 det samme, med hintet for authenticated

Så trin et for hver tabel, appen læser fra browseren:

alter table public.todos enable row level security;
grant select, insert, update, delete on public.todos to authenticated;

Giv kun de kommandoer, browseren har brug for, og kun til authenticated, medmindre gæster skal læse den.

Hvilken Supabase RLS policy lader brugere kun se egne rækker?

Én policy pr. kommando, hver begrænset til loggede brugere:

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 ejer to rækker, B ejer én. Det kom tilbage:

Forespørgsel fra Hvad Status Body
Gæst læs alt 401 permission denied (intet grant til anon)
A læs alt 200 As 2 rækker
A indsæt en række ejet af B 403 new row violates row-level security policy
A opdatér Bs række 200 []
A slet Bs række 200 []
A skift ejeren af egen række til B 403 new row violates row-level security policy

De to 200 []-linjer er dem, du skal huske. RLS afviser ikke en opdatering eller sletning af rækker, du ikke kan se; den filtrerer dem fra, og nul ændrede rækker er en succes. Hvis din app tager “ingen fejl” for “gemt”, fortæller den brugeren, at ændringen virkede, selv om der intet skete. Bed om de ændrede rækker (.select() i supabase-js, Prefer: return=representation over HTTP) og tjek, at der kom én tilbage.

Hvorfor har en update-policy brug for WITH CHECK?

using vælger, hvilke eksisterende rækker en bruger må røre; with check afgør, hvordan rækkerne må se ud bagefter. Jeg sammenlignede tre versioner af den samme update-policy:

Update-policy A giver egen række til B
using (uid = user_id) with check (uid = user_id) 403, afvist
using (uid = user_id), uden with check afvist (Postgres genbruger using som tjek)
using (uid = user_id) with check (true) 200, rækken tilhører nu B

At udelade with check er sikrere, end det ser ud, fordi Postgres falder tilbage på using-udtrykket. with check (true), som nogle generatorer skriver for at “tillade opdateringer”, er det, der lader en bruger flytte en række over på en andens konto. Skriv tjekket helt ud.

Hvordan gør du profiler offentlige, men kun redigerbare af ejeren?

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);

På kørslen med almindelig Postgres: gæsten læste begge profiler, gæstens opdatering ændrede 0 rækker, og As update profiles set display_name = 'x' uden filter ændrede præcis 1 række, As egen. using (true) er fint til et select på data, der skal være offentlige. På enhver anden tabel er det det samme som ingen RLS, og en policy skrevet uden to-klausul gælder også gæster: min gæst læste den “private” række gennem en using (true)-policy uden to.

Hvordan deler du rækker i et team?

Den sædvanlige form: en memberships-tabel og en policy på dataene, der slår en brugers teams op 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 policy er i orden. Lækagen lå ved siden af. Jeg gav memberships den policy, mange tutorials foreslår, “brugere kan indsætte deres eget medlemskab”:

create policy "join any org" on public.memberships
  for insert to authenticated with check (user_id = (select auth.uid()));

Den tjekker, hvem rækken er til, ikke hvilket team. A, medlem af firma 1, sendte én forespørgsel:

Én insert-policy på memberships. Bruger A i firma 1, via REST-API'et. Policy: user_id = auth.uid(): Bruger A → bliver med i firma 2 → 201 Created → læser B payroll; Ingen insert-policy: Bruger A → bliver med i firma 2 → afvist → kun egne projekter.
Figur 1. Min test på en lokal Supabase-stack (CLI 2.109.1), 24. oktober 2026.
Forespørgsel fra A Status Body
læs projekter 200 A roadmap
indsæt medlemskab {user_id: A, org_id: 2} 201 oprettet
læs projekter igen 200 A roadmap, B payroll

Rettelsen er slet ikke at give memberships en insert-policy, så browseren ikke kan skrive til den (på Postgres-kørslen blev den samme insert så afvist med 42501), og at tilføje medlemmer gennem et invitationsflow, der kører på serveren eller i en funktion, som tjekker invitationen. En bruger, der tilhører to firmaer, er den næste fælde; den skrev jeg op i Supabase multi-tenant RLS: bruger i to orgs lækker data.

Hvad hvis en policy skal læse sin egen tabel?

“Medlemmer kan se deres teamkolleger” betyder en policy på memberships, der læser memberships. Postgres afviser det ved forespørgselstidspunktet:

ERROR 42P17: infinite recursion detected in policy for relation "members2"

Flyt opslaget ind i en funktion, der kører som sin ejer, i et skema, API’et ikke eksponerer, 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()));

Bagefter så A begge medlemmer af firma 1, og B så kun sig selv. Hold funktionen ude af public: alt dér kan kaldes gennem API’et.

Hvordan kræver du MFA for følsomme rækker?

Tilføj en restriktiv policy oven på den normale. Restriktive policies kombineres med and, så begge skal bestå:

create policy "mfa only" on public.payouts as restrictive
  for select to authenticated using ((select auth.jwt() ->> 'aal') = 'aal2');

Med den samme bruger læste en session på aal1 0 udbetalinger, og en session på aal2 læste 1.

Hvilke tabeller skal have RLS slået til og ingen policy?

Tabeller, som kun din server rører. RLS slået til uden policy nægter browserrollerne alt: på Postgres-kørslen læste gæsten og A begge 0 rækker, og As insert blev afvist med 42501. At lade RLS være slået fra er det modsatte. En tabel med RLS slået fra og standardgrants, læst med publishable key og uden login:

Forespørgsel fra gæst Status Body
GET /leads 200 begge rækker
DELETE /leads?id=gt.0 200 begge rækker, slettet

Det er det, Advisor-advarslen “RLS disabled in public” handler om.

Kan én policy dække flere kommandoer?

Ikke med en liste. for update, delete er en syntaksfejl (42601: syntax error at or near ","), selv om den stadig står i udgivne samlinger af eksempler. for all er gyldig og anvender ét using og ét with check på hver kommando, hvilket sjældent er det, man vil have. En insert-policy med using fejler også: only WITH CHECK expression allowed for INSERT.

Gør policies forespørgsler langsommere?

Det kan de. Et policy-udtryk kører for hver række, Postgres overvejer. Jeg tidsmålte select * from big som bruger A på 200.000 rækker ejet af 1.000 brugere, hvor A ejede 200 (median af 7 kørsler, Postgres 17.10):

Én ejer-policy, fire måder at skrive den. Millisekunder for at læse 200 egne rækker af 200.000. Median af 7 kørsler. auth.uid() = user_id, uden indeks: 156,30; (select auth.uid()), uden indeks: 7,40; auth.uid() = user_id, med indeks: 0,21; (select auth.uid()), med indeks: 0,05.
Figur 2. Min test på en lokal Postgres 17.10 med Supabases roller genskabt, 24. oktober 2026.
Policy og indeks Mediantid
auth.uid() = user_id, uden indeks 156,3 ms
(select auth.uid()) = user_id, uden indeks 7,4 ms
auth.uid() = user_id, indeks på user_id 0,21 ms
(select auth.uid()) = user_id, indeks 0,05 ms

At pakke kaldet ind i select lader Postgres beregne det én gang pr. forespørgsel i stedet for én gang pr. række; indekset klarer resten. Supabases guide til RLS-ydelse rapporterer samme størrelsesorden på sin egen tabel med 100.000 rækker (179 ms til 9 ms). Mange blogindlæg citerer de tal som deres egne; tallene ovenfor er fra min kørsel.

Hvor skal roller komme fra?

Fra app_metadata eller din egen tabel, aldrig fra user_metadata. En logget bruger kan ændre sin egen user_metadata med supabase.auth.updateUser(), så en policy, der tjekker auth.jwt() -> 'user_metadata' ->> 'role', lader dem give sig selv rollen. En tilmeldings-trigger, der kopierer en rolle fra tilmeldingens metadata, gør det samme; det tilfælde står i Supabase raw_user_meta_data: sådan blev en tilmelding Owner.

Hvordan tjekker du dine egne policies?

  • Det gratis Supabase-sikkerhedstjek læser dit projekt skrivebeskyttet og flager policies, der altid er sande, skrive-policies for gæster, policies der stoler på brugermetadata, og tabeller med RLS slået fra, med rettelses-SQL til hver.
  • Ingen scanner ved, hvilke rækker der tilhører hvem. Til det kører du forespørgsler som to brugere, sådan som denne artikel gør; den fulde metode til en app med roller står i sådan tester du Supabase RLS.
  • Hvad Supabase sikrer for dig, og hvad det overlader til dig, står i Er Supabase sikkert?.
  • De samme ejer- og teamtests, kørt på Appwrite også, står i Appwrite vs Supabase: sikkerhed, samme angreb på begge.

Begrænsninger. Én lokal stack og én bærbar, opfundne data, 24. oktober 2026. Tider afhænger af hardware og tabellens form; forholdene betyder mere end millisekunderne. Storage-policies og Realtime er ikke dækket her.

Usikker på, om dine policies holder?

Jeg kigger gerne på et skema sammen med dig. Ingen salgstale.

Kontakt mig

© 2026 Denys Zaichenko. Original artikel: https://madsalt.tech/da/blog/supabase-rls-policy-examples/