---
title: "Esempi di Supabase RLS policy, testati tramite API"
description: "Esempi di Supabase RLS policy da copiare per righe personali, profili pubblici, team e MFA, ciascuno provato come ospite, proprietario e altro utente."
url: "https://madsalt.tech/it/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/it/about/"
language: "it"
published: 2026-10-27
---

# Esempi di Supabase RLS policy, testati tramite API

TL;DR

Qui sotto ci sono le policy RLS di cui hanno bisogno la maggior parte delle app Supabase, ciascuna eseguita tramite l'API REST come ospite, come proprietario della riga e come altro utente autenticato. Tre risultati vale la pena conoscerli prima di copiare qualsiasi cosa: un update sulla riga di un altro restituisce `200` con una lista vuota, non un errore; una policy di insert dall'aspetto innocuo su una tabella di appartenenze ha permesso a un utente di unirsi a un'altra azienda e di leggerne i progetti; e scrivere `(select auth.uid())` invece di `auth.uid()` ha portato una query da 156 ms a 7,4 ms su 200.000 righe.

Sono Denys, ingegnere full-stack e AI, e la maggior parte delle app che costruisco o sistemo gira su Supabase. Di snippet di Supabase RLS policy ce ne sono ovunque; cosa fanno quando li colpisce una richiesta reale si mostra di rado. Così li ho eseguiti.

**Come ho testato.** La maggior parte delle richieste è passata da uno stack Supabase locale (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) con la publishable key e due utenti reali autenticati, A e B. I casi che non richiedono HTTP, come gli errori di sintassi e le misure di tempo, sono girati su un semplice Postgres 17.10 locale con i ruoli `anon` e `authenticated` di Supabase e `auth.uid()` ricreato. Tutto il 24 ottobre 2026, con righe inventate.

## Prima di ogni policy: la tabella ha un grant?

Una policy decide quali righe vede un ruolo. Un grant decide se il ruolo raggiunge la tabella, e sui progetti Supabase attuali le nuove tabelle non ricevono più automaticamente i grant della Data API (dal 30 ottobre 2026 per ogni progetto). Sul mio stack una tabella con la RLS attiva e senza grant ha risposto così:

| Richiesta                | Stato | Corpo                                                                                  |
| ------------------------ | ----- | -------------------------------------------------------------------------------------- |
| Ospite `GET /invoices`   | `401` | `permission denied for table invoices`, con il suggerimento `GRANT SELECT ... TO anon` |
| Utente A `GET /invoices` | `403` | lo stesso, con il suggerimento per `authenticated`                                     |

Quindi il primo passo per ogni tabella che l'app legge dal browser:

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

Conceda solo i comandi che servono al browser, e solo a `authenticated`, a meno che gli ospiti debbano leggere la tabella.

## Quale Supabase RLS policy mostra agli utenti solo le proprie righe?

Una policy per comando, ciascuna limitata agli utenti autenticati:

```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 possiede due righe, B una. Cosa è tornato:

| Richiesta di | Cosa                                             | Stato | Corpo                                        |
| ------------ | ------------------------------------------------ | ----- | -------------------------------------------- |
| Ospite       | leggere tutto                                    | `401` | permission denied (nessun grant per `anon`)  |
| A            | leggere tutto                                    | `200` | le 2 righe di A                              |
| A            | inserire una riga di B                           | `403` | `new row violates row-level security policy` |
| A            | modificare la riga di B                          | `200` | `[]`                                         |
| A            | eliminare la riga di B                           | `200` | `[]`                                         |
| A            | cambiare in B il proprietario della propria riga | `403` | `new row violates row-level security policy` |

Le due righe `200 []` sono quelle da ricordare. La RLS non rifiuta l'update o la delete di righe che non vede; le filtra, e zero righe modificate è un successo. Se la sua app considera "nessun errore" come "salvato", dirà all'utente che la modifica è andata a buon fine quando non è successo nulla. Chieda le righe modificate (`.select()` in supabase-js, `Prefer: return=representation` via HTTP) e verifichi che ne sia tornata una.

## Perché una policy di update ha bisogno di WITH CHECK?

`using` sceglie quali righe esistenti un utente può toccare; `with check` decide come possono apparire quelle righe dopo. Ho confrontato tre versioni della stessa policy di update:

| Policy di update                                   | A cede la propria riga a B                        |
| -------------------------------------------------- | ------------------------------------------------- |
| `using (uid = user_id) with check (uid = user_id)` | `403`, rifiutato                                  |
| `using (uid = user_id)`, senza `with check`        | rifiutato (Postgres riusa `using` come controllo) |
| `using (uid = user_id) with check (true)`          | `200`, la riga ora appartiene a B                 |

Omettere `with check` è più sicuro di quanto sembri, perché Postgres ripiega sull'espressione `using`. `with check (true)`, che alcuni generatori scrivono per "consentire gli update", è ciò che permette a un utente di spostare una riga nell'account di qualcun altro. Scriva il controllo per intero.

## Come rendere i profili pubblici ma modificabili solo dal proprietario?

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

Nell'esecuzione su Postgres semplice: l'ospite ha letto entrambi i profili, l'update dell'ospite ha modificato 0 righe, e `update profiles set display_name = 'x'` di A senza filtro ha modificato esattamente 1 riga, quella di A. `using (true)` va bene per un select su dati pensati per essere pubblici. Su qualsiasi altra tabella equivale a non avere la RLS, e una policy scritta senza clausola `to` vale anche per gli ospiti: il mio ospite ha letto la riga "privata" attraverso una policy `using (true)` senza `to`.

## Come condividere righe all'interno di un team?

La forma abituale: una tabella `memberships` e una policy sui dati che vi cerca i team di un utente.

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

Quella policy va bene. La falla era accanto. Ho dato a `memberships` la policy che molti tutorial suggeriscono, "gli utenti possono inserire la propria appartenenza":

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

Controlla per chi è la riga, non per quale team. A, membro dell'azienda 1, ha inviato una richiesta:

![Una policy di insert su memberships. Utente A dell'azienda 1, tramite l'API REST. Policy: user_id = auth.uid(): Utente A → entra in azienda 2 → 201 Created → legge B payroll; Nessuna policy di insert: Utente A → entra in azienda 2 → rifiutato → solo suoi progetti.](https://madsalt.tech/charts/it/supabase-rls-policy-examples-join.svg)

*Figura 1. Il mio test su uno stack Supabase locale (CLI 2.109.1), 24 ottobre 2026.*

| Richiesta di A                                    | Stato | Corpo                    |
| ------------------------------------------------- | ----- | ------------------------ |
| leggere i progetti                                | `200` | `A roadmap`              |
| inserire l'appartenenza `{user_id: A, org_id: 2}` | `201` | creata                   |
| rileggere i progetti                              | `200` | `A roadmap`, `B payroll` |

La correzione è non dare a `memberships` alcuna policy di insert, così il browser non può scriverci (nell'esecuzione Postgres lo stesso insert è stato poi rifiutato con `42501`), e aggiungere i membri con un flusso di invito che gira sul server o in una funzione che verifica l'invito. Un utente che appartiene a due aziende è la trappola successiva; l'ho descritta in [Supabase RLS multi-tenant: utente in due org, dati esposti](https://madsalt.tech/it/notes/supabase-rls-user-in-two-organizations/).

## E se una policy deve leggere la propria tabella?

"I membri vedono i colleghi di team" significa una policy su `memberships` che legge `memberships`. Postgres rifiuta al momento della query:

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

Sposti la ricerca in una funzione che gira con i diritti del suo proprietario, in uno schema che l'API non espone, con un `search_path` fisso:

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

Dopo, A vedeva entrambi i membri dell'azienda 1 e B vedeva solo se stesso. Tenga la funzione fuori da `public`: tutto ciò che c'è lì è richiamabile tramite l'API.

## Come richiedere la MFA per righe sensibili?

Aggiunga una policy restrittiva sopra quella normale. Le policy restrittive si combinano con `and`, quindi devono passare entrambe:

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

Con lo stesso utente, una sessione a `aal1` ha letto 0 pagamenti e una sessione a `aal2` ne ha letto 1.

## Quali tabelle dovrebbero avere la RLS attiva e nessuna policy?

Le tabelle che tocca solo il suo server. La RLS attiva senza policy nega tutto ai ruoli del browser: nell'esecuzione Postgres l'ospite e A hanno letto 0 righe, e l'insert di A è stato rifiutato con `42501`. Lasciare la RLS disattivata è l'opposto. Una tabella con la RLS disattivata e i grant predefiniti, letta con la publishable key e senza accesso:

| Richiesta dell'ospite   | Stato | Corpo                        |
| ----------------------- | ----- | ---------------------------- |
| `GET /leads`            | `200` | entrambe le righe            |
| `DELETE /leads?id=gt.0` | `200` | entrambe le righe, eliminate |

È di questo che parla l'avviso dell'Advisor "RLS disabled in public".

## Una sola policy può coprire più comandi?

Non con un elenco. `for update, delete` è un errore di sintassi (`42601: syntax error at or near ","`), anche se compare ancora in raccolte di snippet pubblicate. `for all` è valido e applica un solo `using` e un solo `with check` a ogni comando, il che raramente è ciò che si vuole. Anche una policy di insert con `using` fallisce: `only WITH CHECK expression allowed for INSERT`.

## Le policy rallentano le query?

Possono. Un'espressione di policy viene eseguita per ogni riga che Postgres considera. Ho misurato `select * from big` come utente A su 200.000 righe di 1.000 utenti, 200 delle quali di A (mediana di 7 esecuzioni, Postgres 17.10):

![Una policy solo-proprietario, quattro forme. Millisecondi per leggere 200 righe proprie su 200.000. Mediana di 7 esecuzioni. auth.uid() = user_id, senza indice: 156,30; (select auth.uid()), senza indice: 7,40; auth.uid() = user_id, con indice: 0,21; (select auth.uid()), con indice: 0,05.](https://madsalt.tech/charts/it/supabase-rls-policy-examples.svg)

*Figura 2. Il mio test su Postgres 17.10 locale con i ruoli Supabase ricreati, 24 ottobre 2026.*

| Policy e indice                               | Tempo mediano |
| --------------------------------------------- | ------------- |
| `auth.uid() = user_id`, senza indice          | 156,3 ms      |
| `(select auth.uid()) = user_id`, senza indice | 7,4 ms        |
| `auth.uid() = user_id`, indice su `user_id`   | 0,21 ms       |
| `(select auth.uid()) = user_id`, indice       | 0,05 ms       |

Avvolgere la chiamata in `select` permette a Postgres di calcolarla una volta per query invece che una volta per riga; l'indice fa il resto. La [guida alle prestazioni RLS](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) di Supabase riporta lo stesso ordine di grandezza sulla sua tabella da 100.000 righe (da 179 ms a 9 ms). Molti articoli di blog citano quei numeri come propri; quelli sopra vengono dalla mia esecuzione.

## Da dove devono venire i ruoli?

Da `app_metadata` o da una sua tabella, mai da `user_metadata`. Un utente autenticato può modificare il proprio `user_metadata` con `supabase.auth.updateUser()`, quindi una policy che controlla `auth.jwt() -> 'user_metadata' ->> 'role'` gli permette di assegnarsi il ruolo da solo. Un trigger di registrazione che copia un ruolo dai metadati di registrazione fa lo stesso; quel caso è in [Supabase raw_user_meta_data: un signup diventato Owner](https://madsalt.tech/it/notes/supabase-raw-user-meta-data-role/).

## Come verificare le proprie policy?

- Il [controllo di sicurezza Supabase](https://madsalt.tech/it/tools/supabase-security-check/) gratuito legge il suo progetto in sola lettura e segnala le policy sempre vere, le policy di scrittura per gli ospiti, le policy che si fidano dei metadati utente e le tabelle con la RLS disattivata, con l'SQL di correzione per ciascuna.
- Nessuno scanner sa quali righe appartengono a chi. Per questo si eseguono richieste come due utenti, come in questo articolo; il metodo completo per un'app con ruoli è in [come testare la Supabase RLS](https://madsalt.tech/it/blog/how-to-test-supabase-rls/).
- Cosa Supabase mette in sicurezza per Lei e cosa lascia a Lei è in [Supabase è sicuro?](https://madsalt.tech/it/blog/is-supabase-secure/).
- Gli stessi test sul proprietario e sul team, eseguiti anche su Appwrite, sono in [Sicurezza Appwrite vs Supabase: stessi attacchi su entrambi](https://madsalt.tech/it/blog/appwrite-vs-supabase/).

**Limiti.** Uno stack locale e un portatile, dati inventati, 24 ottobre 2026. I tempi dipendono dall'hardware e dalla forma della tabella; contano più i rapporti dei millisecondi. Le policy di Storage e Realtime non sono trattate qui.

Non è sicuro che le sue policy reggano?

Guardo volentieri uno schema insieme a Lei. Nessuna vendita.

[Mi contatti](https://madsalt.tech/it/#contact)
