Hopp til innhold

Supabase RLS policy-eksempler, testet via API-et

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

Spørsmålet kopieres også. Hvis chatten åpnes tom, lim det inn.

TL;DR

Under står RLS-policyene de fleste Supabase-apper trenger, hver kjørt gjennom REST-API-et som gjest, som radens eier og som en annen innlogget bruker. Tre resultater er verdt å kjenne til før du kopierer noe: en oppdatering av andres rad gir 200 med en tom liste, ikke en feil; en uskyldig utseende insert-policy på en medlemskapstabell lot en bruker bli med i et annet selskap og lese prosjektene dets; og (select auth.uid()) i stedet for auth.uid() tok en spørring på 200 000 rader fra 156 ms til 7,4 ms.

Jeg heter Denys og er full-stack- og AI-utvikler, og de fleste appene jeg bygger eller reparerer kjører på Supabase. Eksempler på en Supabase RLS policy finnes overalt; hva de gjør når en ekte forespørsel treffer dem, vises sjelden. Så jeg kjørte dem.

Slik testet jeg. De fleste forespørslene gikk gjennom en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) med publishable key og to ekte innloggede brukere, A og B. Tilfeller som ikke trenger HTTP, som syntaksfeil og tidsmålinger, kjørte på en vanlig lokal Postgres 17.10 med Supabase sine roller anon og authenticated og gjenskapt auth.uid(). Alt den 24. oktober 2026, med oppdiktede rader.

Før enhver policy: har tabellen et grant?

En policy avgjør hvilke rader en rolle ser. Et grant avgjør om rollen i det hele tatt når tabellen, og på dagens Supabase-prosjekter får nye tabeller ikke lenger automatisk Data API-grants (fra 30. oktober 2026 for alle prosjekter). På stacken min svarte en tabell med RLS på og uten grant slik:

Forespørsel Status Body
Gjest GET /invoices 401 permission denied for table invoices, med hintet GRANT SELECT ... TO anon
Bruker A GET /invoices 403 det samme, med hintet for authenticated

Så steg én for hver tabell appen leser fra nettleseren:

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

Gi bare kommandoene nettleseren trenger, og bare til authenticated, med mindre gjester skal lese den.

Hvilken Supabase RLS policy lar brukere bare se sine egne rader?

Én policy per kommando, hver begrenset til innloggede brukere:

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 eier to rader, B eier én. Dette kom tilbake:

Forespørsel fra Hva Status Body
Gjest les alt 401 permission denied (ingen grant for anon)
A les alt 200 As 2 rader
A sett inn en rad eid av B 403 new row violates row-level security policy
A oppdater Bs rad 200 []
A slett Bs rad 200 []
A bytt eier på egen rad til B 403 new row violates row-level security policy

De to 200 []-linjene er de du skal huske. RLS avviser ikke en oppdatering eller sletting av rader du ikke ser; den filtrerer dem bort, og null endrede rader er en suksess. Hvis appen din tar «ingen feil» for «lagret», forteller den brukeren at endringen virket mens ingenting skjedde. Be om de endrede radene (.select() i supabase-js, Prefer: return=representation over HTTP) og sjekk at én kom tilbake.

Hvorfor trenger en update-policy WITH CHECK?

using velger hvilke eksisterende rader en bruker får røre; with check avgjør hvordan radene får se ut etterpå. Jeg sammenlignet tre versjoner av den samme update-policyen:

Update-policy A gir egen rad til B
using (uid = user_id) with check (uid = user_id) 403, avvist
using (uid = user_id), uten with check avvist (Postgres gjenbruker using som sjekk)
using (uid = user_id) with check (true) 200, raden tilhører nå B

Å utelate with check er tryggere enn det ser ut, fordi Postgres faller tilbake på using-uttrykket. with check (true), som noen generatorer skriver for å «tillate oppdateringer», er det som lar en bruker flytte en rad over på kontoen til en annen. Skriv sjekken helt ut.

Hvordan gjør du profiler offentlige, men bare redigerbare av eieren?

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å kjøringen med vanlig Postgres: gjesten leste begge profilene, gjestens oppdatering endret 0 rader, og As update profiles set display_name = 'x' uten filter endret nøyaktig 1 rad, As egen. using (true) er greit for et select på data som skal være offentlige. På enhver annen tabell er det det samme som ingen RLS, og en policy skrevet uten to-klausul gjelder også gjester: gjesten min leste den «private» raden gjennom en using (true)-policy uten to.

Hvordan deler du rader i et team?

Den vanlige formen: en memberships-tabell og en policy på dataene som slår opp brukerens 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 policyen er i orden. Lekkasjen lå ved siden av. Jeg ga memberships policyen mange veiledninger foreslår, «brukere kan sette inn sitt eget medlemskap»:

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

Den sjekker hvem raden er for, ikke hvilket team. A, medlem av selskap 1, sendte én forespørsel:

Én insert-policy på memberships. Bruker A i selskap 1, via REST-API-et. Policy: user_id = auth.uid(): Bruker A → blir med i selskap 2 → 201 Created → leser B payroll; Ingen insert-policy: Bruker A → blir med i selskap 2 → avvist → bare egne prosjekter.
Figur 1. Min test på en lokal Supabase-stack (CLI 2.109.1), 24. oktober 2026.
Forespørsel fra A Status Body
les prosjekter 200 A roadmap
sett inn medlemskap {user_id: A, org_id: 2} 201 opprettet
les prosjekter igjen 200 A roadmap, B payroll

Løsningen er å ikke gi memberships noen insert-policy i det hele tatt, slik at nettleseren ikke kan skrive til den (på Postgres-kjøringen ble den samme inserten da avvist med 42501), og å legge til medlemmer gjennom en invitasjonsflyt som kjører på serveren eller i en funksjon som sjekker invitasjonen. En bruker som tilhører to selskaper er neste felle; den skrev jeg opp i Supabase RLS: bruker i to organisasjoner lekker data.

Hva hvis en policy må lese sin egen tabell?

«Medlemmer kan se lagkameratene sine» betyr en policy på memberships som leser memberships. Postgres nekter ved spørringstidspunktet:

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

Flytt oppslaget inn i en funksjon som kjører som eieren sin, i et skjema 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()));

Etterpå så A begge medlemmene i selskap 1, og B så bare seg selv. Hold funksjonen utenfor public: alt der kan kalles gjennom API-et.

Hvordan krever du MFA for sensitive rader?

Legg til en restriktiv policy oppå den vanlige. Restriktive policyer kombineres med and, så begge må bestå:

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

Med samme bruker leste en økt på aal1 0 utbetalinger, og en økt på aal2 leste 1.

Hvilke tabeller bør ha RLS på og ingen policy?

Tabeller som bare serveren din rører. RLS på uten policy nekter nettleserrollene alt: på Postgres-kjøringen leste gjesten og A begge 0 rader, og As insert ble avvist med 42501. Å la RLS stå av er det motsatte. En tabell med RLS av og standard grants, lest med publishable key og uten innlogging:

Forespørsel fra gjest Status Body
GET /leads 200 begge radene
DELETE /leads?id=gt.0 200 begge radene, slettet

Det er dette Advisor-advarselen «RLS disabled in public» handler om.

Kan én policy dekke flere kommandoer?

Ikke med en liste. for update, delete er en syntaksfeil (42601: syntax error at or near ","), selv om den fortsatt står i publiserte samlinger av eksempler. for all er gyldig og bruker ett using og ett with check på hver kommando, noe som sjelden er det man vil. En insert-policy med using feiler også: only WITH CHECK expression allowed for INSERT.

Gjør policyer spørringer tregere?

Det kan de. Et policy-uttrykk kjører for hver rad Postgres vurderer. Jeg tidsmålte select * from big som bruker A på 200 000 rader eid av 1 000 brukere, der A eide 200 (median av 7 kjøringer, Postgres 17.10):

Én eier-policy, fire måter å skrive den. Millisekunder for å lese 200 egne rader av 200 000. Median av 7 kjøringer. auth.uid() = user_id, uten indeks: 156,30; (select auth.uid()), uten 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 Supabase sine roller gjenskapt, 24. oktober 2026.
Policy og indeks Mediantid
auth.uid() = user_id, uten indeks 156,3 ms
(select auth.uid()) = user_id, uten indeks 7,4 ms
auth.uid() = user_id, indeks på user_id 0,21 ms
(select auth.uid()) = user_id, indeks 0,05 ms

Å pakke kallet inn i select lar Postgres beregne det én gang per spørring i stedet for én gang per rad; indeksen gjør resten. Supabase sin veiledning om RLS-ytelse rapporterer samme størrelsesorden på sin egen tabell med 100 000 rader (179 ms til 9 ms). Mange blogginnlegg siterer de tallene som sine egne; tallene over er fra kjøringen min.

Hvor skal roller komme fra?

Fra app_metadata eller din egen tabell, aldri fra user_metadata. En innlogget bruker kan endre sin egen user_metadata med supabase.auth.updateUser(), så en policy som sjekker auth.jwt() -> 'user_metadata' ->> 'role' lar dem gi seg selv rollen. En registreringstrigger som kopierer en rolle fra registreringens metadata gjør det samme; det tilfellet står i Supabase raw_user_meta_data: slik ble en registrering Owner.

Hvordan sjekker du dine egne policyer?

  • Den gratis Supabase-sikkerhetssjekken leser prosjektet ditt skrivebeskyttet og flagger policyer som alltid er sanne, skrivepolicyer for gjester, policyer som stoler på brukermetadata, og tabeller med RLS av, med rettelses-SQL for hver.
  • Ingen skanner vet hvilke rader som tilhører hvem. Til det kjører du forespørsler som to brukere, slik denne artikkelen gjør; den fullstendige metoden for en app med roller står i hvordan du tester Supabase RLS.
  • Hva Supabase sikrer for deg og hva det overlater til deg, står i Er Supabase trygt?.
  • De samme eier- og teamtestene, kjørt på Appwrite også, står i Appwrite vs Supabase sikkerhet: samme angrep på begge.

Begrensninger. Én lokal stack og én bærbar, oppdiktede data, 24. oktober 2026. Tider avhenger av maskinvare og tabellform; forholdstallene betyr mer enn millisekundene. Storage-policyer og Realtime er ikke dekket her.

Usikker på om policyene dine holder?

Jeg ser gjerne på et skjema sammen med deg. Ingen salgstale.

Ta kontakt

© 2026 Denys Zaichenko. Opprinnelig artikkel: https://madsalt.tech/no/blog/supabase-rls-policy-examples/