---
title: "Supabase RLS policy-exempel, testade via API:t"
description: "Supabase RLS policy-exempel att kopiera för egna rader, publika profiler, team och MFA, vart och ett kört som gäst, ägare och annan användare."
url: "https://madsalt.tech/sv/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/sv/about/"
language: "sv"
published: 2026-10-27
---

# Supabase RLS policy-exempel, testade via API:t

TL;DR

Nedan finns de RLS-policyer som de flesta Supabase-appar behöver, var och en körd genom REST-API:t som gäst, som radens ägare och som en annan inloggad användare. Tre resultat är värda att känna till innan du kopierar något: en uppdatering av någon annans rad ger `200` med en tom lista, inte ett fel; en harmlöst utseende insert-policy på en medlemskapstabell lät en användare gå med i ett annat företag och läsa dess projekt; och att skriva `(select auth.uid())` i stället för `auth.uid()` tog en fråga på 200 000 rader från 156 ms till 7,4 ms.

Jag heter Denys och är full-stack- och AI-utvecklare, och de flesta appar jag bygger eller reparerar körs på Supabase. Exempel på en Supabase RLS policy finns överallt; vad de gör när en riktig förfrågan träffar dem visas sällan. Så jag körde dem.

**Så testade jag.** De flesta förfrågningar gick genom en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) med publishable key och två riktiga inloggade användare, A och B. Fall som inte kräver HTTP, som syntaxfel och tidsmätningar, kördes på en vanlig lokal Postgres 17.10 med Supabases roller `anon` och `authenticated` och återskapad `auth.uid()`. Allt den 24 oktober 2026, med påhittade rader.

## Före varje policy: har tabellen ett grant?

En policy avgör vilka rader en roll ser. Ett grant avgör om rollen alls når tabellen, och på aktuella Supabase-projekt får nya tabeller inte längre automatiskt Data API-grants (från och med den 30 oktober 2026 för alla projekt). På min stack svarade en tabell med RLS på och utan grant så här:

| Förfrågan                   | Status | Body                                                                          |
| --------------------------- | ------ | ----------------------------------------------------------------------------- |
| Gäst `GET /invoices`        | `401`  | `permission denied for table invoices`, med tipset `GRANT SELECT ... TO anon` |
| Användare A `GET /invoices` | `403`  | samma, med tipset för `authenticated`                                         |

Så steg ett för varje tabell som appen läser från webbläsaren:

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

Ge bara de kommandon webbläsaren behöver, och bara till `authenticated`, om inte gäster ska läsa den.

## Vilken Supabase RLS policy låter användare bara se sina egna rader?

En policy per kommando, var och en begränsad till inloggade användare:

```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 äger två rader, B äger en. Det som kom tillbaka:

| Förfrågan från | Vad                          | Status | Body                                         |
| -------------- | ---------------------------- | ------ | -------------------------------------------- |
| Gäst           | läs allt                     | `401`  | permission denied (inget grant för `anon`)   |
| A              | läs allt                     | `200`  | A:s 2 rader                                  |
| A              | infoga en rad som ägs av B   | `403`  | `new row violates row-level security policy` |
| A              | uppdatera B:s rad            | `200`  | `[]`                                         |
| A              | ta bort B:s rad              | `200`  | `[]`                                         |
| A              | byt ägare på egen rad till B | `403`  | `new row violates row-level security policy` |

De två `200 []`-raderna är de att minnas. RLS avvisar inte en uppdatering eller borttagning av rader du inte kan se; den filtrerar bort dem, och noll ändrade rader räknas som lyckat. Om din app tar "inget fel" som "sparat" säger den till användaren att ändringen gick igenom när ingenting hände. Be om de ändrade raderna (`.select()` i supabase-js, `Prefer: return=representation` över HTTP) och kontrollera att en kom tillbaka.

## Varför behöver en update-policy WITH CHECK?

`using` väljer vilka befintliga rader en användare får röra; `with check` avgör hur de raderna får se ut efteråt. Jag jämförde tre versioner av samma update-policy:

| Update-policy                                      | A ger egen rad till B                              |
| -------------------------------------------------- | -------------------------------------------------- |
| `using (uid = user_id) with check (uid = user_id)` | `403`, nekad                                       |
| `using (uid = user_id)`, utan `with check`         | nekad (Postgres återanvänder `using` som kontroll) |
| `using (uid = user_id) with check (true)`          | `200`, raden tillhör nu B                          |

Att utelämna `with check` är säkrare än det ser ut, eftersom Postgres faller tillbaka på `using`-uttrycket. `with check (true)`, som vissa generatorer skriver för att "tillåta uppdateringar", är det som låter en användare flytta en rad till någon annans konto. Skriv ut kontrollen i sin helhet.

## Hur gör du profiler publika men bara redigerbara av ägaren?

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

I körningen på vanlig Postgres: gästen läste båda profilerna, gästens uppdatering ändrade 0 rader, och A:s `update profiles set display_name = 'x'` utan filter ändrade exakt 1 rad, A:s egen. `using (true)` är fint för ett select på data som är avsedda att vara publika. På vilken annan tabell som helst är det detsamma som ingen RLS, och en policy skriven utan `to`-klausul gäller även gäster: min gäst läste den "privata" raden genom en `using (true)`-policy utan `to`.

## Hur delar du rader inom ett team?

Den vanliga formen: en `memberships`-tabell och en policy på datan som slår upp en användares team 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 policyn är fin. Läckan fanns bredvid. Jag gav `memberships` den policy som många guider föreslår, "användare kan infoga sitt eget medlemskap":

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

Den kontrollerar vem raden är för, inte vilket team. A, medlem i företag 1, skickade en förfrågan:

![En insert-policy på memberships. Användare A i företag 1, via REST-API:t. Policy: user_id = auth.uid(): Användare A → går med i företag 2 → 201 Created → läser B payroll; Ingen insert-policy: Användare A → går med i företag 2 → nekad → bara egna projekt.](https://madsalt.tech/charts/sv/supabase-rls-policy-examples-join.svg)

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

| Förfrågan från A                            | Status | Body                     |
| ------------------------------------------- | ------ | ------------------------ |
| läs projekt                                 | `200`  | `A roadmap`              |
| infoga medlemskap `{user_id: A, org_id: 2}` | `201`  | skapat                   |
| läs projekt igen                            | `200`  | `A roadmap`, `B payroll` |

Åtgärden är att inte ge `memberships` någon insert-policy alls, så att webbläsaren inte kan skriva till den (i Postgres-körningen nekades samma insert då med `42501`), och att lägga till medlemmar via ett inbjudningsflöde som körs på servern eller i en funktion som kontrollerar inbjudan. En användare som tillhör två företag är nästa fälla; den skrev jag om i [Supabase RLS: användare i två organisationer läcker](https://madsalt.tech/sv/notes/supabase-rls-user-in-two-organizations/).

## Vad om en policy behöver läsa sin egen tabell?

"Medlemmar kan se sina lagkamrater" betyder en policy på `memberships` som läser `memberships`. Postgres vägrar vid frågetillfället:

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

Flytta uppslaget till en funktion som körs som sin ägare, i ett schema som API:t inte exponerar, 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()));
```

Efter det såg A båda medlemmarna i företag 1 och B såg bara sig själv. Håll funktionen utanför `public`: allt där går att anropa via API:t.

## Hur kräver du MFA för känsliga rader?

Lägg till en restriktiv policy ovanpå den vanliga. Restriktiva policyer kombineras med `and`, så båda måste godkänna:

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

Med samma användare läste en session på `aal1` 0 utbetalningar och en session på `aal2` läste 1.

## Vilka tabeller ska ha RLS på och ingen policy?

Tabeller som bara din server rör. RLS på utan policy nekar webbläsarrollerna allt: i Postgres-körningen läste gästen och A båda 0 rader, och A:s insert nekades med `42501`. Att lämna RLS av är raka motsatsen. En tabell med RLS av och standardgrants, läst med publishable key och utan inloggning:

| Förfrågan från gäst     | Status | Body                    |
| ----------------------- | ------ | ----------------------- |
| `GET /leads`            | `200`  | båda raderna            |
| `DELETE /leads?id=gt.0` | `200`  | båda raderna, borttagna |

Det är vad Advisor-varningen "RLS disabled in public" handlar om.

## Kan en policy täcka flera kommandon?

Inte med en lista. `for update, delete` är ett syntaxfel (`42601: syntax error at or near ","`), även om det fortfarande finns i publicerade samlingar av exempel. `for all` är giltigt och tillämpar ett `using` och ett `with check` på varje kommando, vilket sällan är vad man vill. En insert-policy med `using` misslyckas också: `only WITH CHECK expression allowed for INSERT`.

## Gör policyer frågor långsammare?

Det kan de. Ett policy-uttryck körs för varje rad Postgres överväger. Jag tidsmätte `select * from big` som användare A på 200 000 rader som ägs av 1 000 användare, där A äger 200 (median av 7 körningar, Postgres 17.10):

![En ägar-policy, fyra sätt att skriva den. Millisekunder för att läsa 200 egna rader av 200 000. Median av 7 körningar. auth.uid() = user_id, utan index: 156,30; (select auth.uid()), utan index: 7,40; auth.uid() = user_id, med index: 0,21; (select auth.uid()), med index: 0,05.](https://madsalt.tech/charts/sv/supabase-rls-policy-examples.svg)

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

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

Att linda in anropet i `select` låter Postgres beräkna det en gång per fråga i stället för en gång per rad; indexet gör resten. Supabases [guide om RLS-prestanda](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) rapporterar samma storleksordning på sin egen tabell med 100 000 rader (179 ms till 9 ms). Många blogginlägg citerar de siffrorna som sina egna; siffrorna ovan är från min körning.

## Varifrån ska roller komma?

Från `app_metadata` eller din egen tabell, aldrig från `user_metadata`. En inloggad användare kan ändra sin egen `user_metadata` med `supabase.auth.updateUser()`, så en policy som kontrollerar `auth.jwt() -> 'user_metadata' ->> 'role'` låter dem ge sig själva rollen. En registreringstrigger som kopierar en roll från registreringens metadata gör detsamma; det fallet finns i [Supabase raw_user_meta_data: så blev en registrering Owner](https://madsalt.tech/sv/notes/supabase-raw-user-meta-data-role/).

## Hur kontrollerar du dina egna policyer?

- Den kostnadsfria [Supabase-säkerhetskontrollen](https://madsalt.tech/sv/tools/supabase-security-check/) läser ditt projekt skrivskyddat och flaggar policyer som alltid är sanna, skrivpolicyer för gäster, policyer som litar på användarmetadata och tabeller med RLS av, med åtgärds-SQL för var och en.
- Ingen skanner vet vilka rader som tillhör vem. För det kör du förfrågningar som två användare, som den här artikeln gör; den fullständiga metoden för en app med roller finns i [hur du testar Supabase RLS](https://madsalt.tech/sv/blog/how-to-test-supabase-rls/).
- Vad Supabase säkrar åt dig och vad det lämnar åt dig finns i [Är Supabase säkert?](https://madsalt.tech/sv/blog/is-supabase-secure/).
- De samma ägar- och teamtesterna, körda även på Appwrite, finns i [Appwrite vs Supabase säkerhet: samma attacker på båda](https://madsalt.tech/sv/blog/appwrite-vs-supabase/).

**Begränsningar.** En lokal stack och en bärbar dator, påhittad data, 24 oktober 2026. Tider beror på hårdvara och tabellens form; förhållandena betyder mer än millisekunderna. Storage-policyer och Realtime ingår inte här.

Osäker på om dina policyer håller?

Jag tittar gärna på ett schema tillsammans med dig. Ingen säljpitch.

[Hör av dig](https://madsalt.tech/sv/#contact)
