Vai al contenuto

Esempi di Supabase RLS policy, testati tramite API

Visualizza come MarkdownApri in ClaudeApri in ChatGPTApri in Perplexity

Anche la domanda viene copiata. Se la chat si apre vuota, la incolli.

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:

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:

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?

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.

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

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

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:

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:

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:

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

Come verificare le proprie policy?

  • Il controllo di sicurezza Supabase 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.
  • Cosa Supabase mette in sicurezza per Lei e cosa lascia a Lei è in Supabase è sicuro?.
  • Gli stessi test sul proprietario e sul team, eseguiti anche su Appwrite, sono in Sicurezza Appwrite vs Supabase: stessi attacchi su entrambi.

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

© 2026 Denys Zaichenko. Articolo originale: https://madsalt.tech/it/blog/supabase-rls-policy-examples/