TL;DR
Nižšie sú politiky RLS, ktoré potrebuje väčšina aplikácií na Supabase. Každú som poslal cez REST API ako hosť, ako vlastník riadku a ako iný prihlásený používateľ. Tri výsledky stoja za to poznať skôr, než niečo skopírujete: update cudzieho riadku vráti 200 s prázdnym zoznamom, nie chybu; nevinne vyzerajúca insert politika na tabuľke členstiev dovolila používateľovi vstúpiť do inej firmy a čítať jej projekty; a zápis (select auth.uid()) namiesto auth.uid() zrýchlil jeden dopyt zo 156 ms na 7,4 ms na 200 000 riadkoch.
Som Denys, full-stack a AI inžinier, a väčšina aplikácií, ktoré stavám alebo opravujem, beží na Supabase. Ukážok, ako napísať Supabase RLS policy, je všade dosť; čo urobia, keď na ne dopadne skutočná požiadavka, sa ukazuje zriedka. Tak som ich spustil.
Ako som testoval. Väčšina požiadaviek išla cez lokálny Supabase stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) s publishable key a dvoma skutočnými prihlásenými používateľmi, A a B. Prípady, ktoré nepotrebujú HTTP, napríklad syntaktické chyby a časovanie, bežali na čistom lokálnom Postgrese 17.10 s rolami anon a authenticated zo Supabase a znovu vytvorenou funkciou auth.uid(). Všetko 24. októbra 2026, s vymyslenými riadkami.
Ešte pred politikou: má tabuľka grant?
Politika rozhoduje, ktoré riadky rola vidí. Grant rozhoduje, či sa rola k tabuľke vôbec dostane, a na aktuálnych projektoch Supabase už nové tabuľky granty Data API automaticky nedostávajú (od 30. októbra 2026 pri všetkých projektoch). Na mojom stacku odpovedala tabuľka so zapnutým RLS a bez grantu takto:
| Požiadavka | Status | Telo |
|---|---|---|
Hosť GET /invoices |
401 |
permission denied for table invoices, s nápovedou GRANT SELECT ... TO anon |
Používateľ A GET /invoices |
403 |
to isté, s nápovedou pre authenticated |
Takže krok jeden pri každej tabuľke, ktorú aplikácia číta z prehliadača:
alter table public.todos enable row level security;
grant select, insert, update, delete on public.todos to authenticated;
Udeľte len príkazy, ktoré prehliadač potrebuje, a len role authenticated, ak tabuľku nemajú čítať aj hostia.
Ako napísať Supabase RLS policy, aby používatelia videli len svoje riadky?
Jedna politika na príkaz, každá obmedzená na prihlásených používateľov:
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 riadky, B jeden. Čo sa vrátilo:
| Požiadavka od | Čo | Status | Telo |
|---|---|---|---|
| Hosť | čítať všetko | 401 |
permission denied (žiadny grant pre anon) |
| A | čítať všetko | 200 |
2 riadky používateľa A |
| A | vložiť riadok patriaci B | 403 |
new row violates row-level security policy |
| A | upraviť riadok používateľa B | 200 |
[] |
| A | zmazať riadok používateľa B | 200 |
[] |
| A | zmeniť vlastníka svojho riadku na B | 403 |
new row violates row-level security policy |
Dva riadky 200 [] sú tie, ktoré si zapamätajte. RLS update ani delete riadkov, ktoré nevidíte, neodmietne; odfiltruje ich a nula zmenených riadkov je úspech. Ak vaša aplikácia berie „žiadna chyba“ ako „uložené“, povie používateľovi, že úprava prebehla, hoci sa nestalo nič. Vyžiadajte si zmenené riadky (.select() v supabase-js, Prefer: return=representation cez HTTP) a overte, že sa nejaký vrátil.
Prečo update politika potrebuje WITH CHECK?
using vyberá, ktorých existujúcich riadkov sa používateľ smie dotknúť; with check rozhoduje, ako môžu tie riadky vyzerať potom. Porovnal som tri verzie tej istej update politiky:
| Update politika | A odovzdá svoj riadok používateľovi B |
|---|---|
using (uid = user_id) with check (uid = user_id) |
403, zamietnuté |
using (uid = user_id), bez with check |
zamietnuté (Postgres použije using ako kontrolu) |
using (uid = user_id) with check (true) |
200, riadok teraz patrí používateľovi B |
Vynechať with check je bezpečnejšie, než to vyzerá, pretože Postgres siahne po výraze z using. with check (true), ktoré niektoré generátory píšu, aby „povolili updaty“, je presne to, čo používateľovi dovolí presunúť riadok do cudzieho účtu. Napíšte kontrolu celú.
Ako urobiť profily verejné, ale upraviteľné len vlastníkom?
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 behu na čistom Postgrese: hosť prečítal oba profily, update hosťa zmenil 0 riadkov a update profiles set display_name = 'x' od používateľa A bez filtra zmenil presne 1 riadok, A vlastný. using (true) je v poriadku pre select nad dátami, ktoré majú byť verejné. Na akejkoľvek inej tabuľke je to to isté ako žiadne RLS a politika napísaná bez klauzuly to platí aj pre hostí: môj hosť prečítal „súkromný“ riadok cez politiku using (true), ktorá to nemala.
Ako zdieľať riadky vnútri tímu?
Obvyklý tvar: tabuľka memberships a politika na dátach, ktorá v nej vyhľadá tímy používateľa.
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())));
Tá politika je v poriadku. Únik bol vedľa nej. Tabuľke memberships som dal politiku, ktorú odporúča rad tutoriálov, „používatelia môžu vložiť svoje vlastné členstvo“:
create policy "join any org" on public.memberships
for insert to authenticated with check (user_id = (select auth.uid()));
Kontroluje, komu riadok patrí, nie do ktorého tímu. A, člen firmy 1, poslal jednu požiadavku:
| Požiadavka používateľa A | Status | Telo |
|---|---|---|
| čítať projekty | 200 |
A roadmap |
vložiť členstvo {user_id: A, org_id: 2} |
201 |
vytvorené |
| znovu čítať projekty | 200 |
A roadmap, B payroll |
Oprava spočíva v tom, že memberships nedostane žiadnu insert politiku, takže do nej prehliadač nemôže zapisovať (na behu na Postgrese bol rovnaký insert potom zamietnutý s 42501), a členov sa pridáva cez pozvánkový tok, ktorý beží na serveri alebo vo funkcii kontrolujúcej pozvánku. Používateľ patriaci do dvoch firiem je ďalšia pasca; napísal som ju v článku Supabase multi-tenant RLS: únik dát u používateľa v 2 org.
Čo ak politika potrebuje čítať vlastnú tabuľku?
„Členovia vidia svojich kolegov z tímu“ znamená politiku na memberships, ktorá číta memberships. Postgres ju pri dopyte odmietne:
ERROR 42P17: infinite recursion detected in policy for relation "members2"
Presuňte vyhľadávanie do funkcie, ktorá beží ako jej vlastník, v schéme, ktorú 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 videl oboch členov firmy 1 a B videl len seba. Funkciu nechajte mimo public: čokoľvek tam je, dá sa volať cez API.
Ako vyžadovať MFA pri citlivých riadkoch?
Pridajte k bežnej politike reštriktívnu politiku. Reštriktívne politiky sa spájajú cez and, takže musia prejsť obe:
create policy "mfa only" on public.payouts as restrictive
for select to authenticated using ((select auth.jwt() ->> 'aal') = 'aal2');
S rovnakým používateľom prečítala session na úrovni aal1 0 výplat a session na úrovni aal2 1.
Ktoré tabuľky majú mať RLS zapnuté a žiadnu politiku?
Tabuľky, ktorých sa dotýka len váš server. RLS zapnuté bez politiky odoprie prehliadačovým rolám všetko: na behu na Postgrese prečítali hosť aj A 0 riadkov a insert od používateľa A bol zamietnutý s 42501. Nechať RLS vypnuté je opak. Tabuľka s vypnutým RLS a predvolenými grantmi, čítaná s publishable key a bez prihlásenia:
| Požiadavka hosťa | Status | Telo |
|---|---|---|
GET /leads |
200 |
oba riadky |
DELETE /leads?id=gt.0 |
200 |
oba riadky, zmazané |
O tom je varovanie Advisora „RLS disabled in public“.
Môže jedna politika pokryť viac príkazov?
Nie zoznamom. for update, delete je syntaktická chyba (42601: syntax error at or near ","), hoci sa stále objavuje vo zverejnených zbierkach ukážok. for all je platné a použije jedno using a jedno with check na každý príkaz, čo málokedy chcete. Insert politika s using tiež zlyhá: only WITH CHECK expression allowed for INSERT.
Spomaľujú politiky dopyty?
Môžu. Výraz politiky sa vyhodnocuje pre každý riadok, ktorý Postgres zvažuje. Zmeral som select * from big ako používateľ A na 200 000 riadkoch patriacich 1 000 používateľom, z ktorých A vlastnil 200 (medián zo 7 behov, Postgres 17.10):
| 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 |
Obalenie volania do select nechá Postgres vypočítať hodnotu raz na dopyt namiesto raz na riadok; zvyšok urobí index. Sprievodca výkonom RLS od Supabase uvádza rovnaký rád zmeny na vlastnej tabuľke so 100 000 riadkami (179 ms na 9 ms). Rad blogových článkov tie čísla cituje ako vlastné; tie vyššie sú z môjho behu.
Odkiaľ majú pochádzať role?
Z app_metadata alebo z vlastnej tabuľky, nikdy z user_metadata. Prihlásený používateľ si svoje user_metadata môže zmeniť cez supabase.auth.updateUser(), takže politika, ktorá kontroluje auth.jwt() -> 'user_metadata' ->> 'role', mu dovolí prideliť si rolu sám. To isté urobí registračný trigger, ktorý kopíruje rolu z metadát registrácie; ten prípad je v článku Supabase raw_user_meta_data: registrácia sa stala Ownerom.
Ako skontrolovať vlastné politiky?
- Bezplatná Kontrola zabezpečenia Supabase číta váš projekt len na čítanie a označí politiky, ktoré platia vždy, zápisové politiky pre hostí, politiky dôverujúce metadátam používateľa a tabuľky s vypnutým RLS, ku každému s opravným SQL.
- Žiadny skener nevie, ktoré riadky komu patria. Na to posielajte požiadavky ako dvaja používatelia, tak ako v tomto článku; celá metóda pre aplikáciu s rolami je v článku Ako testovať Supabase RLS: 30 z 87 oprávnení nerobilo nič.
- Čo Supabase zabezpečuje za vás a čo necháva na vás, je v článku Je Supabase bezpečný? Čo pokrýva a čo musíte vy.
- Rovnaké testy vlastníka a tímu, spustené aj na Appwrite, sú v článku Appwrite vs Supabase: bezpečnosť, rovnaké útoky na oba.
Limity. Jeden lokálny stack a jeden notebook, vymyslené dáta, 24. októbra 2026. Časovanie závisí od hardvéru a tvaru tabuľky; dôležitejšie než milisekundy sú pomery. Politiky pre Storage a Realtime tu nie sú pokryté.