Přejít na obsah

Supabase RLS policy: příklady otestované přes API

Zobrazit jako MarkdownOtevřít v ClaudeOtevřít v ChatGPTOtevřít v Perplexity

Otázka se zkopíruje také. Pokud se chat otevře prázdný, vložte ji.

TL;DR

Níže jsou politiky RLS, které potřebuje většina aplikací na Supabase. Každou jsem poslal přes REST API jako host, jako vlastník řádku a jako jiný přihlášený uživatel. Tři výsledky stojí za to znát dřív, než cokoli zkopírujete: update cizího řádku vrátí 200 s prázdným seznamem, ne chybu; nevinně vypadající insert politika na tabulce členství dovolila uživateli vstoupit do jiné firmy a číst její projekty; a zápis (select auth.uid()) místo auth.uid() zrychlil jeden dotaz ze 156 ms na 7,4 ms na 200 000 řádcích.

Jsem Denys, full-stack a AI inženýr, a většina aplikací, které stavím nebo opravuji, běží na Supabase. Ukázek, jak napsat Supabase RLS policy, je všude dost; co udělají, když na ně dopadne skutečný požadavek, se ukazuje zřídka. Tak jsem je spustil.

Jak jsem testoval. Většina požadavků šla přes lokální Supabase stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) s publishable key a dvěma skutečnými přihlášenými uživateli, A a B. Případy, které nepotřebují HTTP, například syntaktické chyby a časování, běžely na čistém lokálním Postgresu 17.10 s rolemi anon a authenticated ze Supabase a znovu vytvořenou funkcí auth.uid(). Vše 24. října 2026, s vymyšlenými řádky.

Ještě před politikou: má tabulka grant?

Politika rozhoduje, které řádky role vidí. Grant rozhoduje, zda se role k tabulce vůbec dostane, a na aktuálních projektech Supabase už nové tabulky grantům Data API automaticky nedostávají (od 30. října 2026 u všech projektů). Na mém stacku odpověděla tabulka se zapnutým RLS a bez grantu takto:

Požadavek Status Tělo
Host GET /invoices 401 permission denied for table invoices, s nápovědou GRANT SELECT ... TO anon
Uživatel A GET /invoices 403 totéž, s nápovědou pro authenticated

Takže krok jedna u každé tabulky, kterou aplikace čte z prohlížeče:

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

Udělte jen příkazy, které prohlížeč potřebuje, a jen roli authenticated, pokud tabulku nemají číst i hosté.

Jak napsat Supabase RLS policy, aby uživatelé viděli jen své řádky?

Jedna politika na příkaz, každá omezená na přihlášené uživatele:

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 vlastní dva řádky, B jeden. Co se vrátilo:

Požadavek od Co Status Tělo
Host číst vše 401 permission denied (žádný grant pro anon)
A číst vše 200 2 řádky uživatele A
A vložit řádek patřící B 403 new row violates row-level security policy
A upravit řádek uživatele B 200 []
A smazat řádek uživatele B 200 []
A změnit vlastníka svého řádku na B 403 new row violates row-level security policy

Dva řádky 200 [] jsou ty, které si zapamatujte. RLS update ani delete řádků, které nevidíte, neodmítne; vyfiltruje je a nula změněných řádků je úspěch. Pokud vaše aplikace bere „žádná chyba“ jako „uloženo“, řekne uživateli, že úprava proběhla, ačkoli se nestalo nic. Vyžádejte si změněné řádky (.select() v supabase-js, Prefer: return=representation přes HTTP) a ověřte, že se nějaký vrátil.

Proč update politika potřebuje WITH CHECK?

using vybírá, kterých existujících řádků se uživatel smí dotknout; with check rozhoduje, jak mohou ty řádky vypadat potom. Porovnal jsem tři verze téže update politiky:

Update politika A předá svůj řádek uživateli B
using (uid = user_id) with check (uid = user_id) 403, zamítnuto
using (uid = user_id), bez with check zamítnuto (Postgres použije using jako kontrolu)
using (uid = user_id) with check (true) 200, řádek teď patří uživateli B

Vynechat with check je bezpečnější, než to vypadá, protože Postgres sáhne po výrazu z using. with check (true), které některé generátory píší, aby „povolily updaty“, je přesně to, co uživateli dovolí přesunout řádek do cizího účtu. Napište kontrolu celou.

Jak udělat profily veřejné, ale upravitelné jen vlastníkem?

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

Na běhu na čistém Postgresu: host přečetl oba profily, update hosta změnil 0 řádků a update profiles set display_name = 'x' od uživatele A bez filtru změnil přesně 1 řádek, A vlastní. using (true) je v pořádku pro select nad daty, která mají být veřejná. Na jakékoli jiné tabulce je to totéž jako žádné RLS a politika napsaná bez klauzule to platí i pro hosty: můj host přečetl „soukromý“ řádek přes politiku using (true), která to neměla.

Jak sdílet řádky uvnitř týmu?

Obvyklý tvar: tabulka memberships a politika na datech, která v ní vyhledá týmy uživatele.

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

Ta politika je v pořádku. Únik byl vedle ní. Tabulce memberships jsem dal politiku, kterou doporučuje řada tutoriálů, „uživatelé mohou vložit své vlastní členství“:

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

Kontroluje, komu řádek patří, ne do kterého týmu. A, člen firmy 1, poslal jeden požadavek:

Jedna insert politika na memberships. Uživatel A firmy 1 přes REST API. Politika: user_id = auth.uid(): Uživatel A → vstup do firmy 2 → 201 Created → čte mzdy firmy B; Bez insert politiky: Uživatel A → vstup do firmy 2 → zamítnuto → jen vlastní projekty.
Obrázek 1. Můj test na lokálním stacku Supabase (CLI 2.109.1), 24. října 2026.
Požadavek uživatele A Status Tělo
číst projekty 200 A roadmap
vložit členství {user_id: A, org_id: 2} 201 vytvořeno
znovu číst projekty 200 A roadmap, B payroll

Oprava spočívá v tom, že memberships nedostane žádnou insert politiku, takže do ní prohlížeč nemůže zapisovat (na běhu na Postgresu byl stejný insert pak zamítnut s 42501), a členy se přidávají přes pozvánkový tok, který běží na serveru nebo ve funkci kontrolující pozvánku. Uživatel patřící do dvou firem je další past; napsal jsem ji v článku Supabase multi-tenant RLS: únik dat u uživatele ve dvou org.

Co když politika potřebuje číst vlastní tabulku?

„Členové vidí své kolegy z týmu“ znamená politiku na memberships, která čte memberships. Postgres ji při dotazu odmítne:

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

Přesuňte vyhledávání do funkce, která běží jako její vlastník, ve schématu, které API nevystavuje, s pevným 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()));

Potom A viděl oba členy firmy 1 a B viděl jen sebe. Funkci nechte mimo public: cokoli tam je, jde volat přes API.

Jak vyžadovat MFA u citlivých řádků?

Přidejte k běžné politice restriktivní politiku. Restriktivní politiky se spojují přes and, takže musí projít obě:

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

Se stejným uživatelem přečetla session na úrovni aal1 0 výplat a session na úrovni aal2 1.

Které tabulky mají mít RLS zapnuté a žádnou politiku?

Tabulky, kterých se dotýká jen váš server. RLS zapnuté bez politiky odepře prohlížečovým rolím všechno: na běhu na Postgresu přečetli host i A 0 řádků a insert od uživatele A byl zamítnut s 42501. Nechat RLS vypnuté je opak. Tabulka s vypnutým RLS a výchozími granty, čtená s publishable key a bez přihlášení:

Požadavek hosta Status Tělo
GET /leads 200 oba řádky
DELETE /leads?id=gt.0 200 oba řádky, smazány

O tom je varování Advisoru „RLS disabled in public“.

Může jedna politika pokrýt více příkazů?

Ne seznamem. for update, delete je syntaktická chyba (42601: syntax error at or near ","), přestože se pořád objevuje ve zveřejněných sbírkách ukázek. for all je platné a použije jedno using a jedno with check na každý příkaz, což málokdy chcete. Insert politika s using také selže: only WITH CHECK expression allowed for INSERT.

Zpomalují politiky dotazy?

Mohou. Výraz politiky se vyhodnocuje pro každý řádek, který Postgres zvažuje. Změřil jsem select * from big jako uživatel A na 200 000 řádcích patřících 1 000 uživatelům, z nichž A vlastnil 200 (medián ze 7 běhů, Postgres 17.10):

Jedna politika jen pro vlastníka, čtyři zápisy. Milisekundy na přečtení 200 vlastních řádků z 200 000. Medián ze 7 běhů. auth.uid() = user_id, bez indexu: 156,30; (select auth.uid()), bez indexu: 7,40; auth.uid() = user_id, s indexem: 0,21; (select auth.uid()), s indexem: 0,05.
Obrázek 2. Můj test na lokálním Postgresu 17.10 s vytvořenými rolemi Supabase, 24. října 2026.
Politika a index Medián času
auth.uid() = user_id, bez indexu 156,3 ms
(select auth.uid()) = user_id, bez indexu 7,4 ms
auth.uid() = user_id, index na user_id 0,21 ms
(select auth.uid()) = user_id, index 0,05 ms

Obalení volání do select nechá Postgres vypočítat hodnotu jednou na dotaz místo jednou na řádek; zbytek udělá index. Průvodce výkonem RLS od Supabase uvádí stejný řád změny na vlastní tabulce se 100 000 řádky (179 ms na 9 ms). Řada blogových článků ta čísla cituje jako vlastní; ta výše jsou z mého běhu.

Odkud mají pocházet role?

Z app_metadata nebo z vlastní tabulky, nikdy z user_metadata. Přihlášený uživatel si svá user_metadata může změnit přes supabase.auth.updateUser(), takže politika, která kontroluje auth.jwt() -> 'user_metadata' ->> 'role', mu dovolí přidělit si roli sám. Totéž udělá registrační trigger, který kopíruje roli z metadat registrace; ten případ je v článku Supabase raw_user_meta_data: jak se registrace stala Ownerem.

Jak zkontrolovat vlastní politiky?

Limity. Jeden lokální stack a jeden notebook, vymyšlená data, 24. října 2026. Časování závisí na hardwaru a tvaru tabulky; důležitější než milisekundy jsou poměry. Politiky pro Storage a Realtime tu nejsou pokryty.

Nejste si jistí, že vaše politiky drží?

Rád se s vámi podívám na schéma. Bez nabídky.

Ozvěte se

© 2026 Denys Zaichenko. Původní článek: https://madsalt.tech/cs/blog/supabase-rls-policy-examples/