---
title: "Supabase RLS policy-eksempler, hver testet via API"
description: "Supabase RLS policy-eksempler til at kopiere: egne rækker, offentlige profiler, teams og MFA, hver kørt som gæst, ejer og anden bruger."
url: "https://madsalt.tech/da/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/da/about/"
language: "da"
published: 2026-10-27
---

# Supabase RLS policy-eksempler, hver testet via API

TL;DR

Nedenfor står de RLS-policies, som de fleste Supabase-apps har brug for, hver kørt gennem REST-API'et som gæst, som rækkens ejer og som en anden logget ind bruger. Tre resultater er værd at kende, før du kopierer noget: en opdatering af en andens række giver `200` med en tom liste, ikke en fejl; en uskyldigt udseende insert-policy på en medlemskabstabel lod en bruger blive medlem af et andet firma og læse dets projekter; og `(select auth.uid())` i stedet for `auth.uid()` bragte en forespørgsel på 200.000 rækker fra 156 ms til 7,4 ms.

Jeg hedder Denys og er full-stack- og AI-udvikler, og de fleste apps, jeg bygger eller reparerer, kører på Supabase. Eksempler på en Supabase RLS policy findes overalt; hvad de gør, når en rigtig forespørgsel rammer dem, bliver sjældent vist. Så jeg kørte dem.

**Sådan testede jeg.** De fleste forespørgsler gik gennem en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) med publishable key og to rigtige loggede brugere, A og B. Tilfælde, der ikke kræver HTTP, som syntaksfejl og tidsmålinger, kørte på en almindelig lokal Postgres 17.10 med Supabases roller `anon` og `authenticated` og genskabt `auth.uid()`. Alt den 24. oktober 2026, med opfundne rækker.

## Før enhver policy: har tabellen et grant?

En policy afgør, hvilke rækker en rolle ser. Et grant afgør, om rollen overhovedet når tabellen, og på nuværende Supabase-projekter får nye tabeller ikke længere automatisk Data API-grants (fra den 30. oktober 2026 for alle projekter). På min stack svarede en tabel med RLS slået til og uden grant sådan:

| Forespørgsel             | Status | Body                                                                          |
| ------------------------ | ------ | ----------------------------------------------------------------------------- |
| Gæst `GET /invoices`     | `401`  | `permission denied for table invoices`, med hintet `GRANT SELECT ... TO anon` |
| Bruger A `GET /invoices` | `403`  | det samme, med hintet for `authenticated`                                     |

Så trin et for hver tabel, appen læser fra browseren:

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

Giv kun de kommandoer, browseren har brug for, og kun til `authenticated`, medmindre gæster skal læse den.

## Hvilken Supabase RLS policy lader brugere kun se egne rækker?

Én policy pr. kommando, hver begrænset til loggede brugere:

```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 ejer to rækker, B ejer én. Det kom tilbage:

| Forespørgsel fra | Hvad                             | Status | Body                                         |
| ---------------- | -------------------------------- | ------ | -------------------------------------------- |
| Gæst             | læs alt                          | `401`  | permission denied (intet grant til `anon`)   |
| A                | læs alt                          | `200`  | As 2 rækker                                  |
| A                | indsæt en række ejet af B        | `403`  | `new row violates row-level security policy` |
| A                | opdatér Bs række                 | `200`  | `[]`                                         |
| A                | slet Bs række                    | `200`  | `[]`                                         |
| A                | skift ejeren af egen række til B | `403`  | `new row violates row-level security policy` |

De to `200 []`-linjer er dem, du skal huske. RLS afviser ikke en opdatering eller sletning af rækker, du ikke kan se; den filtrerer dem fra, og nul ændrede rækker er en succes. Hvis din app tager "ingen fejl" for "gemt", fortæller den brugeren, at ændringen virkede, selv om der intet skete. Bed om de ændrede rækker (`.select()` i supabase-js, `Prefer: return=representation` over HTTP) og tjek, at der kom én tilbage.

## Hvorfor har en update-policy brug for WITH CHECK?

`using` vælger, hvilke eksisterende rækker en bruger må røre; `with check` afgør, hvordan rækkerne må se ud bagefter. Jeg sammenlignede tre versioner af den samme update-policy:

| Update-policy                                      | A giver egen række til B                     |
| -------------------------------------------------- | -------------------------------------------- |
| `using (uid = user_id) with check (uid = user_id)` | `403`, afvist                                |
| `using (uid = user_id)`, uden `with check`         | afvist (Postgres genbruger `using` som tjek) |
| `using (uid = user_id) with check (true)`          | `200`, rækken tilhører nu B                  |

At udelade `with check` er sikrere, end det ser ud, fordi Postgres falder tilbage på `using`-udtrykket. `with check (true)`, som nogle generatorer skriver for at "tillade opdateringer", er det, der lader en bruger flytte en række over på en andens konto. Skriv tjekket helt ud.

## Hvordan gør du profiler offentlige, men kun redigerbare af ejeren?

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

På kørslen med almindelig Postgres: gæsten læste begge profiler, gæstens opdatering ændrede 0 rækker, og As `update profiles set display_name = 'x'` uden filter ændrede præcis 1 række, As egen. `using (true)` er fint til et select på data, der skal være offentlige. På enhver anden tabel er det det samme som ingen RLS, og en policy skrevet uden `to`-klausul gælder også gæster: min gæst læste den "private" række gennem en `using (true)`-policy uden `to`.

## Hvordan deler du rækker i et team?

Den sædvanlige form: en `memberships`-tabel og en policy på dataene, der slår en brugers teams op i den.

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

Den policy er i orden. Lækagen lå ved siden af. Jeg gav `memberships` den policy, mange tutorials foreslår, "brugere kan indsætte deres eget medlemskab":

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

Den tjekker, hvem rækken er til, ikke hvilket team. A, medlem af firma 1, sendte én forespørgsel:

![Én insert-policy på memberships. Bruger A i firma 1, via REST-API'et. Policy: user_id = auth.uid(): Bruger A → bliver med i firma 2 → 201 Created → læser B payroll; Ingen insert-policy: Bruger A → bliver med i firma 2 → afvist → kun egne projekter.](https://madsalt.tech/charts/da/supabase-rls-policy-examples-join.svg)

*Figur 1. Min test på en lokal Supabase-stack (CLI 2.109.1), 24. oktober 2026.*

| Forespørgsel fra A                          | Status | Body                     |
| ------------------------------------------- | ------ | ------------------------ |
| læs projekter                               | `200`  | `A roadmap`              |
| indsæt medlemskab `{user_id: A, org_id: 2}` | `201`  | oprettet                 |
| læs projekter igen                          | `200`  | `A roadmap`, `B payroll` |

Rettelsen er slet ikke at give `memberships` en insert-policy, så browseren ikke kan skrive til den (på Postgres-kørslen blev den samme insert så afvist med `42501`), og at tilføje medlemmer gennem et invitationsflow, der kører på serveren eller i en funktion, som tjekker invitationen. En bruger, der tilhører to firmaer, er den næste fælde; den skrev jeg op i [Supabase multi-tenant RLS: bruger i to orgs lækker data](https://madsalt.tech/da/notes/supabase-rls-user-in-two-organizations/).

## Hvad hvis en policy skal læse sin egen tabel?

"Medlemmer kan se deres teamkolleger" betyder en policy på `memberships`, der læser `memberships`. Postgres afviser det ved forespørgselstidspunktet:

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

Flyt opslaget ind i en funktion, der kører som sin ejer, i et skema, API'et ikke eksponerer, med fast `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()));
```

Bagefter så A begge medlemmer af firma 1, og B så kun sig selv. Hold funktionen ude af `public`: alt dér kan kaldes gennem API'et.

## Hvordan kræver du MFA for følsomme rækker?

Tilføj en restriktiv policy oven på den normale. Restriktive policies kombineres med `and`, så begge skal bestå:

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

Med den samme bruger læste en session på `aal1` 0 udbetalinger, og en session på `aal2` læste 1.

## Hvilke tabeller skal have RLS slået til og ingen policy?

Tabeller, som kun din server rører. RLS slået til uden policy nægter browserrollerne alt: på Postgres-kørslen læste gæsten og A begge 0 rækker, og As insert blev afvist med `42501`. At lade RLS være slået fra er det modsatte. En tabel med RLS slået fra og standardgrants, læst med publishable key og uden login:

| Forespørgsel fra gæst   | Status | Body                  |
| ----------------------- | ------ | --------------------- |
| `GET /leads`            | `200`  | begge rækker          |
| `DELETE /leads?id=gt.0` | `200`  | begge rækker, slettet |

Det er det, Advisor-advarslen "RLS disabled in public" handler om.

## Kan én policy dække flere kommandoer?

Ikke med en liste. `for update, delete` er en syntaksfejl (`42601: syntax error at or near ","`), selv om den stadig står i udgivne samlinger af eksempler. `for all` er gyldig og anvender ét `using` og ét `with check` på hver kommando, hvilket sjældent er det, man vil have. En insert-policy med `using` fejler også: `only WITH CHECK expression allowed for INSERT`.

## Gør policies forespørgsler langsommere?

Det kan de. Et policy-udtryk kører for hver række, Postgres overvejer. Jeg tidsmålte `select * from big` som bruger A på 200.000 rækker ejet af 1.000 brugere, hvor A ejede 200 (median af 7 kørsler, Postgres 17.10):

![Én ejer-policy, fire måder at skrive den. Millisekunder for at læse 200 egne rækker af 200.000. Median af 7 kørsler. auth.uid() = user_id, uden indeks: 156,30; (select auth.uid()), uden indeks: 7,40; auth.uid() = user_id, med indeks: 0,21; (select auth.uid()), med indeks: 0,05.](https://madsalt.tech/charts/da/supabase-rls-policy-examples.svg)

*Figur 2. Min test på en lokal Postgres 17.10 med Supabases roller genskabt, 24. oktober 2026.*

| Policy og indeks                             | Mediantid |
| -------------------------------------------- | --------- |
| `auth.uid() = user_id`, uden indeks          | 156,3 ms  |
| `(select auth.uid()) = user_id`, uden indeks | 7,4 ms    |
| `auth.uid() = user_id`, indeks på `user_id`  | 0,21 ms   |
| `(select auth.uid()) = user_id`, indeks      | 0,05 ms   |

At pakke kaldet ind i `select` lader Postgres beregne det én gang pr. forespørgsel i stedet for én gang pr. række; indekset klarer resten. Supabases [guide til RLS-ydelse](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) rapporterer samme størrelsesorden på sin egen tabel med 100.000 rækker (179 ms til 9 ms). Mange blogindlæg citerer de tal som deres egne; tallene ovenfor er fra min kørsel.

## Hvor skal roller komme fra?

Fra `app_metadata` eller din egen tabel, aldrig fra `user_metadata`. En logget bruger kan ændre sin egen `user_metadata` med `supabase.auth.updateUser()`, så en policy, der tjekker `auth.jwt() -> 'user_metadata' ->> 'role'`, lader dem give sig selv rollen. En tilmeldings-trigger, der kopierer en rolle fra tilmeldingens metadata, gør det samme; det tilfælde står i [Supabase raw_user_meta_data: sådan blev en tilmelding Owner](https://madsalt.tech/da/notes/supabase-raw-user-meta-data-role/).

## Hvordan tjekker du dine egne policies?

- Det gratis [Supabase-sikkerhedstjek](https://madsalt.tech/da/tools/supabase-security-check/) læser dit projekt skrivebeskyttet og flager policies, der altid er sande, skrive-policies for gæster, policies der stoler på brugermetadata, og tabeller med RLS slået fra, med rettelses-SQL til hver.
- Ingen scanner ved, hvilke rækker der tilhører hvem. Til det kører du forespørgsler som to brugere, sådan som denne artikel gør; den fulde metode til en app med roller står i [sådan tester du Supabase RLS](https://madsalt.tech/da/blog/how-to-test-supabase-rls/).
- Hvad Supabase sikrer for dig, og hvad det overlader til dig, står i [Er Supabase sikkert?](https://madsalt.tech/da/blog/is-supabase-secure/).
- De samme ejer- og teamtests, kørt på Appwrite også, står i [Appwrite vs Supabase: sikkerhed, samme angreb på begge](https://madsalt.tech/da/blog/appwrite-vs-supabase/).

**Begrænsninger.** Én lokal stack og én bærbar, opfundne data, 24. oktober 2026. Tider afhænger af hardware og tabellens form; forholdene betyder mere end millisekunderne. Storage-policies og Realtime er ikke dækket her.

Usikker på, om dine policies holder?

Jeg kigger gerne på et skema sammen med dig. Ingen salgstale.

[Kontakt mig](https://madsalt.tech/da/#contact)
