---
title: "Supabase RLS policy: příklady otestované přes API"
description: "Hotové Supabase RLS policy pro vlastní řádky, veřejné profily, týmy a MFA. Každá vyzkoušená jako host, vlastník a jiný uživatel, s výsledky."
url: "https://madsalt.tech/cs/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/cs/about/"
language: "cs"
published: 2026-10-27
---

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

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:

```sql
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:

```sql
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?

```sql
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.

```sql
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í“:

```sql
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.](https://madsalt.tech/charts/cs/supabase-rls-policy-examples-join.svg)

*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](https://madsalt.tech/cs/notes/supabase-rls-user-in-two-organizations/).

## 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:

```text
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`:

```sql
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ě:

```sql
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.](https://madsalt.tech/charts/cs/supabase-rls-policy-examples.svg)

*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](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) 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](https://madsalt.tech/cs/notes/supabase-raw-user-meta-data-role/).

## Jak zkontrolovat vlastní politiky?

- Bezplatná [Kontrola zabezpečení Supabase](https://madsalt.tech/cs/tools/supabase-security-check/) čte váš projekt jen pro čtení a označí politiky, které platí vždy, zápisové politiky pro hosty, politiky důvěřující metadatům uživatele a tabulky s vypnutým RLS, ke každému s opravným SQL.
- Žádný skener neví, které řádky komu patří. K tomu posílejte požadavky jako dva uživatelé, tak jako v tomto článku; celá metoda pro aplikaci s rolemi je v článku [Jak testovat Supabase RLS: 30 z 87 oprávnění nedělalo nic](https://madsalt.tech/cs/blog/how-to-test-supabase-rls/).
- Co Supabase zajišťuje za vás a co nechává na vás, je v článku [Je Supabase bezpečný? Co pokrývá a co musíte vy](https://madsalt.tech/cs/blog/is-supabase-secure/).
- Stejné testy vlastníka a týmu, spuštěné i na Appwrite, jsou v článku [Appwrite vs Supabase: bezpečnost, stejné útoky na oba](https://madsalt.tech/cs/blog/appwrite-vs-supabase/).

**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](https://madsalt.tech/cs/#contact)
