---
title: "Supabase RLS policy -esimerkit testattuna API:n kautta"
description: "Kopioitavia Supabase RLS policy -esimerkkejä: omat rivit, julkiset profiilit, tiimit ja MFA, ajettuna vieraana, omistajana ja toisena käyttäjänä, tuloksineen."
url: "https://madsalt.tech/fi/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/fi/about/"
language: "fi"
published: 2026-10-27
---

# Supabase RLS policy -esimerkit testattuna API:n kautta

TL;DR

Alla ovat RLS-politiikat, joita useimmat Supabase-sovellukset tarvitsevat, ja jokainen on ajettu REST API:n kautta vieraana, rivin omistajana ja toisena kirjautuneena käyttäjänä. Kolme tulosta kannattaa tietää ennen kuin kopioit mitään: toisen käyttäjän rivin update palauttaa `200` ja tyhjän listan, ei virhettä; harmittoman näköinen insert-politiikka jäsentaulussa antoi käyttäjän liittyä toiseen yritykseen ja lukea sen projektit; ja kun kirjoitin `(select auth.uid())` lauseen `auth.uid()` sijaan, yksi kysely nopeutui 156 ms:sta 7,4 ms:iin 200 000 rivillä.

Olen Denys, full-stack- ja tekoälyinsinööri, ja suurin osa rakentamistani tai korjaamistani sovelluksista pyörii Supabasella. Politiikkojen koodinpätkiä on kaikkialla, mutta se, mitä ne tekevät, kun oikea pyyntö osuu niihin, näytetään harvoin. Joten ajoin ne. Tässä ovat Supabase RLS policy -esimerkit, joista aloitan itse, ja kunkin tulos.

**Näin testasin.** Suurin osa pyynnöistä kulki paikallisen Supabase-stackin läpi (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) julkisella avaimella ja kahdella oikealla kirjautuneella käyttäjällä, A:lla ja B:llä. Tapaukset, jotka eivät tarvitse HTTP:tä, kuten syntaksivirheet ja ajoitukset, ajoin tavallisella paikallisella Postgres 17.10:llä, jossa Supabasen `anon`- ja `authenticated`-roolit ja `auth.uid()` oli luotu uudelleen. Kaikki 24. lokakuuta 2026, keksityillä riveillä.

## Ennen yhtäkään politiikkaa: onko taululla grant?

Politiikka päättää, mitkä rivit rooli näkee. Grant päättää, pääseekö rooli taulun luo lainkaan, ja nykyisissä Supabase-projekteissa uudet taulut eivät enää saa Data API -granteja automaattisesti (30. lokakuuta 2026 alkaen kaikissa projekteissa). Omassa stackissani taulu, jossa RLS oli päällä mutta granttia ei ollut, vastasi:

| Pyyntö                     | Tila  | Runko                                                                       |
| -------------------------- | ----- | --------------------------------------------------------------------------- |
| Vieras `GET /invoices`     | `401` | `permission denied for table invoices`, vihjeenä `GRANT SELECT ... TO anon` |
| Käyttäjä A `GET /invoices` | `403` | sama, vihjeenä `authenticated`-roolin grant                                 |

Joten jokaiselle taululle, jota sovellus lukee selaimesta, ensimmäinen askel on:

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

Anna vain ne komennot, joita selain tarvitsee, ja vain roolille `authenticated`, ellei vieraiden tarvitse lukea taulua.

## Miten käyttäjät näkevät vain omat rivinsä?

Yksi politiikka komentoa kohden, jokainen rajattu kirjautuneisiin käyttäjiin:

```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 omistaa kaksi riviä, B yhden. Tulokset:

| Pyytäjä | Mitä                            | Tila  | Runko                                          |
| ------- | ------------------------------- | ----- | ---------------------------------------------- |
| Vieras  | lue kaikki                      | `401` | permission denied (`anon`-roolilla ei grantia) |
| A       | lue kaikki                      | `200` | A:n 2 riviä                                    |
| A       | insert B:n omistamaan riviin    | `403` | `new row violates row-level security policy`   |
| A       | update B:n riviin               | `200` | `[]`                                           |
| A       | delete B:n riviin               | `200` | `[]`                                           |
| A       | vaihda oman rivin omistajaksi B | `403` | `new row violates row-level security policy`   |

Kaksi `200 []` -riviä kannattaa muistaa. RLS ei hylkää updatea tai deleteä riveille, joita et näe; se suodattaa ne pois, ja nolla muuttunutta riviä on onnistuminen. Jos sovelluksesi pitää "ei virhettä" merkkinä siitä, että tallennus onnistui, se kertoo käyttäjälle muutoksen onnistuneen, vaikka mitään ei tapahtunut. Pyydä muuttuneet rivit takaisin (`.select()` supabase-js:ssä, `Prefer: return=representation` HTTP:llä) ja tarkista, että yksi palasi.

## Miksi update-politiikka tarvitsee WITH CHECKin?

`using` valitsee, mihin olemassa oleviin riveihin käyttäjä saa koskea; `with check` päättää, miltä rivit saavat näyttää jälkeenpäin. Vertasin saman update-politiikan kolmea versiota:

| Update-politiikka                                  | A antaa oman rivinsä B:lle                             |
| -------------------------------------------------- | ------------------------------------------------------ |
| `using (uid = user_id) with check (uid = user_id)` | `403`, estetty                                         |
| `using (uid = user_id)`, ei `with check` -osaa     | estetty (Postgres käyttää `using`-ehtoa myös checkinä) |
| `using (uid = user_id) with check (true)`          | `200`, rivi kuuluu nyt B:lle                           |

`with check`in jättäminen pois on turvallisempaa kuin miltä näyttää, koska Postgres palaa `using`-lausekkeeseen. Käyttäjän päästää siirtämään rivin jonkun toisen tilille `with check (true)`, jonka jotkin generaattorit kirjoittavat "sallimaan updatet". Kirjoita check kokonaan.

## Miten profiilit tehdään julkisiksi mutta vain omistajan muokattaviksi?

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

Tavallisella Postgresilla: vieras luki molemmat profiilit, vieraan update muutti 0 riviä, ja A:n `update profiles set display_name = 'x'` ilman suodatinta muutti täsmälleen 1 rivin, A:n oman. `using (true)` sopii selectiin datalle, jonka on tarkoitus olla julkista. Millä tahansa muulla taululla se vastaa sitä, ettei RLS:ää ole, ja ilman `to`-lausetta kirjoitettu politiikka koskee myös vieraita: vieraani luki "yksityisen" rivin `using (true)` -politiikalla, jossa ei ollut `to`-osaa.

## Miten rivejä jaetaan tiimin sisällä?

Tavallinen muoto: `memberships`-taulu ja datan politiikka, joka hakee käyttäjän tiimit sieltä.

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

Tuo politiikka on kunnossa. Vuoto oli sen vieressä. Annoin `memberships`-taululle politiikan, jota monet oppaat ehdottavat, "käyttäjät voivat lisätä oman jäsenyytensä":

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

Se tarkistaa, kenelle rivi on, ei mihin tiimiin. A, yrityksen 1 jäsen, lähetti yhden pyynnön:

![Yksi insert-politiikka taulussa memberships. Yrityksen 1 käyttäjä A, REST API:n kautta. Politiikka: user_id = auth.uid(): Käyttäjä A → liittyy yritykseen 2 → 201 Created → lukee B:n palkat; Ei insert-politiikkaa: Käyttäjä A → liittyy yritykseen 2 → estetty → vain omat projektit.](https://madsalt.tech/charts/fi/supabase-rls-policy-examples-join.svg)

*Kuva 1. Oma testini paikallisella Supabase-stackilla (CLI 2.109.1), 24. lokakuuta 2026.*

| A:n pyyntö                                | Tila  | Runko                    |
| ----------------------------------------- | ----- | ------------------------ |
| lue projektit                             | `200` | `A roadmap`              |
| insert jäsenyys `{user_id: A, org_id: 2}` | `201` | luotu                    |
| lue projektit uudelleen                   | `200` | `A roadmap`, `B payroll` |

Korjaus on olla antamatta `memberships`-taululle lainkaan insert-politiikkaa, jolloin selain ei voi kirjoittaa siihen (tavallisella Postgresilla sama insert estettiin silloin virheellä `42501`), ja lisätä jäsenet kutsuvirran kautta, joka ajetaan palvelimella tai funktiossa, joka tarkistaa kutsun. Käyttäjä, joka kuuluu kahteen yritykseen, on seuraava ansa; kirjoitin siitä artikkelissa [Supabase RLS: kahden organisaation käyttäjä vuotaa dataa](https://madsalt.tech/fi/notes/supabase-rls-user-in-two-organizations/).

## Entä jos politiikan pitää lukea omaa taulunsa?

"Jäsenet näkevät tiimikaverinsa" tarkoittaa politiikkaa taulussa `memberships`, joka lukee taulua `memberships`. Postgres kieltäytyy kyselyn ajon aikana:

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

Siirrä haku funktioon, joka ajetaan omistajansa oikeuksilla, skeemassa, jota API ei julkaise, kiinteällä `search_path`illa:

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

Sen jälkeen A näki yrityksen 1 molemmat jäsenet ja B näki vain itsensä. Pidä funktio poissa skeemasta `public`: kaikki siellä oleva on kutsuttavissa API:n kautta.

## Miten MFA vaaditaan arkaluonteisille riveille?

Lisää rajoittava politiikka tavallisen päälle. Rajoittavat politiikat yhdistetään `and`-operaattorilla, joten molempien on mentävä läpi:

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

Samalla käyttäjällä `aal1`-istunto luki 0 maksua ja `aal2`-istunto luki 1.

## Millä tauluilla RLS on päällä ilman politiikkaa?

Tauluilla, joihin koskee vain palvelimesi. RLS päällä ilman politiikkaa kieltää kaiken selaimen rooleilta: tavallisella Postgresilla vieras ja A lukivat 0 riviä, ja A:n insert estettiin virheellä `42501`. RLS:n jättäminen pois päältä on päinvastainen tilanne. Taulu, jossa RLS on pois päältä ja oletusgrantit, luettuna julkisella avaimella ilman kirjautumista:

| Vieraan pyyntö          | Tila  | Runko                     |
| ----------------------- | ----- | ------------------------- |
| `GET /leads`            | `200` | molemmat rivit            |
| `DELETE /leads?id=gt.0` | `200` | molemmat rivit, poistettu |

Tästä Advisorin varoituksessa "RLS disabled in public" on kyse.

## Voiko yksi politiikka kattaa useita komentoja?

Ei listalla. `for update, delete` on syntaksivirhe (`42601: syntax error at or near ","`), vaikka se esiintyy yhä julkaistuissa koodinpätkäkokoelmissa. `for all` on kelvollinen ja soveltaa yhtä `using`- ja yhtä `with check` -ehtoa kaikkiin komentoihin, mitä harvoin halutaan. Myös insert-politiikka, jolla on `using`, epäonnistuu: `only WITH CHECK expression allowed for INSERT`.

## Hidastavatko politiikat kyselyjä?

Voivat hidastaa. Politiikan lauseke ajetaan jokaiselle riville, jota Postgres harkitsee. Ajoitin `select * from big` käyttäjänä A 200 000 rivillä, jotka omistaa 1 000 käyttäjää, A:lla niistä 200 (7 ajon mediaani, Postgres 17.10):

![Yksi omistajapolitiikka, neljä kirjoitustapaa. Millisekuntia 200 oman rivin lukuun 200 000:sta. 7 ajon mediaani. auth.uid() = user_id, ei indeksiä: 156,30; (select auth.uid()), ei indeksiä: 7,40; auth.uid() = user_id, indeksillä: 0,21; (select auth.uid()), indeksillä: 0,05.](https://madsalt.tech/charts/fi/supabase-rls-policy-examples.svg)

*Kuva 2. Oma testini paikallisella Postgres 17.10:llä, jossa Supabasen roolit oli luotu uudelleen, 24. lokakuuta 2026.*

| Politiikka ja indeksi                                 | Mediaani |
| ----------------------------------------------------- | -------- |
| `auth.uid() = user_id`, ei indeksiä                   | 156,3 ms |
| `(select auth.uid()) = user_id`, ei indeksiä          | 7,4 ms   |
| `auth.uid() = user_id`, indeksi sarakkeella `user_id` | 0,21 ms  |
| `(select auth.uid()) = user_id`, indeksi              | 0,05 ms  |

Kutsun kietominen `select`-lauseeseen antaa Postgresin laskea sen kerran kyselyä kohden eikä kerran riviä kohden; indeksi hoitaa lopun. Supabasen [RLS-suorituskykyopas](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) raportoi saman suuruusluokan muutoksen omassa 100 000 rivin taulussaan (179 ms:sta 9 ms:iin). Moni blogikirjoitus siteeraa nuo luvut ominaan; yllä olevat ovat omasta ajostani.

## Mistä roolien pitäisi tulla?

Kentästä `app_metadata` tai omasta taulustasi, ei koskaan kentästä `user_metadata`. Kirjautunut käyttäjä voi muuttaa oman `user_metadata`nsa komennolla `supabase.auth.updateUser()`, joten politiikka, joka tarkistaa `auth.jwt() -> 'user_metadata' ->> 'role'`, antaa hänen myöntää roolin itselleen. Rekisteröitymistrigger, joka kopioi roolin rekisteröitymisen metadatasta, tekee saman; se tapaus on artikkelissa [Supabase raw_user_meta_data: rekisteröityjästä tuli Owner](https://madsalt.tech/fi/notes/supabase-raw-user-meta-data-role/).

## Miten tarkistat omat politiikkasi?

- Ilmainen [Supabasen tietoturvatarkistus](https://madsalt.tech/fi/tools/supabase-security-check/) lukee projektisi vain luku -tilassa ja liputtaa aina tosi olevat politiikat, vieraiden kirjoituspolitiikat, käyttäjän metadataan luottavat politiikat ja taulut, joilla RLS on pois päältä, kullekin korjaus-SQL:llä.
- Mikään skanneri ei tiedä, mitkä rivit kuuluvat kenellekin. Sitä varten aja pyyntöjä kahtena käyttäjänä, kuten tämä artikkeli tekee; koko menetelmä rooleja käyttävälle sovellukselle on artikkelissa [Supabase RLS:n testaus](https://madsalt.tech/fi/blog/how-to-test-supabase-rls/).
- Mitä Supabase suojaa puolestasi ja mitä se jättää sinulle, löytyy artikkelista [onko Supabase turvallinen?](https://madsalt.tech/fi/blog/is-supabase-secure/).
- Samat omistaja- ja tiimitestit ajettuna myös Appwritella löytyvät artikkelista [Appwrite vs Supabase tietoturva: samat hyökkäykset](https://madsalt.tech/fi/blog/appwrite-vs-supabase/).

**Rajoitukset.** Yksi paikallinen stack ja yksi kannettava, keksittyä dataa, 24. lokakuuta 2026. Ajoitukset riippuvat laitteistosta ja taulun muodosta; suhteet ovat tärkeämpiä kuin millisekunnit. Storagen politiikkoja ja Realtimea ei käsitellä tässä.

Etkö ole varma, kestävätkö politiikkasi?

Katson mielelläni skeemaa kanssasi. Ei myyntipuhetta.

[Ota yhteyttä](https://madsalt.tech/fi/#contact)
