Basi della sicurezza
Attiva Row Level Security su ogni tabella Supabase, poi verificalo
Attivare Row Level Security su Supabase senza una policy chiude del tutto la tabella. Una policy senza il flag non fa nulla. Ecco lo SQL, e la verifica.

In breve
- Attivare Row Level Security su Supabase senza una policy chiude del tutto una tabella, e scrivere una policy senza attivare il flag non fa proprio nulla. Ogni tabella ha bisogno di entrambe le cose.
- Tre forme di policy coprono quasi tutto quello che crea un costruttore di IA: righe che appartengono a una persona, righe che chiunque può leggere e righe che la tua app scrive per conto di un visitatore.
- Poi verifica da fuori la tua app e senza login, perché quella è la richiesta che fa uno sconosciuto, ed è l'unica che ti dice cosa fanno le tue policy invece di cosa dicono.
Ti hanno detto di attivare Row Level Security, oppure il tuo costruttore di IA l'ha nominato di sfuggita mentre sistemava altro. Il tuo progetto Supabase ha da qualche parte fra quattro e quaranta tabelle, e non sai quali siano coperte.
L'istruzione che trovi ovunque è una riga di SQL per tabella, e fin lì è giusta. Quello davanti a cui quasi ogni guida si ferma è il passo dopo. Una policy che esiste non è una policy che funziona, e niente nella tua dashboard ti mostrerà la differenza. Quindi attivare Row Level Security su Supabase sono tre lavori e non uno: mettere il flag su tutte le tabelle, scrivere le due o tre policy che rimettono in piedi la tua app, e poi chiedere al tuo database quello che gli chiederebbe uno sconosciuto.
Cosa fa davvero attivare Row Level Security?
Fa consultare a Postgres le tue regole prima che consegni una riga. Con il flag spento non ci sono regole da consultare, quindi la risposta a ogni richiesta è tutto.
Immagina un bibliotecario che va a prendere quello che chiedi. Row Level Security è l'istruzione di leggere un biglietto su di te prima di riempire il carrello. Il biglietto è la tua policy, e può dire che questa persona può prendere i libri che ha scritto, oppure che chiunque può prendere qualunque cosa. Con l'istruzione in vigore e nessun biglietto ancora scritto, il bibliotecario torna con il carrello vuoto e non spiega perché.
È proprio quest'ultima parte a far inciampare, e decide che aspetto avrà una verifica più avanti. Row Level Security filtra le righe. Non rifiuta le richieste. Una tabella che non hai il permesso di leggere risponde con un elenco vuoto e un codice di successo, non con un errore né con una schermata di accesso. Alla tua app non viene mai detto che è stata respinta. Non riceve nulla e disegna una schermata bianca.
Quindi mettere il flag su un progetto che non ha ancora nessuna policy non chiude fuori gli sconosciuti dai tuoi dati. Chiude fuori tutti, la tua app compresa, finché non dici chi può vedere cosa.
Come attivo RLS su tutte le tabelle Supabase in una volta?
Un ciclo, eseguito una volta nel SQL Editor. Percorre ogni tabella del tuo schema public e mette il flag ovunque sia spento.
Parti dal vedere a che punto sei. Questo elenca le tue tabelle e dice se ognuna ce l'ha adesso:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;
Ogni riga in cui rowsecurity dice false è una tabella che consegna il suo
contenuto a chiunque abbia la chiave pubblicabile che viaggia con la tua app. Se
quell'elenco è corto, falle una alla volta e guarda cosa si rompe:
alter table public.orders enable row level security;
Se non è corto, questo se ne occupa tutto:
do $$
declare t record;
begin
for t in
select tablename from pg_tables where schemaname = 'public'
loop
execute format(
'alter table public.%I enable row level security', t.tablename
);
end loop;
end $$;
Esegui quello e la tua app diventa bianca. Supabase lo dice chiaramente nella sua documentazione: i dati diventano irraggiungibili attraverso l'API con una chiave pubblicabile finché non vengono definite delle policy. È il flag che fa il suo mestiere, ed è il motivo per cui la sezione successiva è quella da tenere aperta prima di premere esegui.
Le tre policy che ti servono davvero
Quasi ogni tabella in un'app come la tua ha una di tre forme: righe che appartengono a una persona, righe che chiunque può leggere e righe che la tua app scrive per conto di un visitatore. Eccole tutte, pronte da incollare e rinominare.
Righe che appartengono a una persona. Ordini, messaggi, elementi salvati, qualsiasi cosa con un proprietario.
create policy "read own orders"
on public.orders for select
to authenticated
using ( (select auth.uid()) = user_id );
auth.uid() è l'id di chi ha fatto l'accesso su quella richiesta. user_id è
la colonna della tua tabella che registra il proprietario, quindi controlla il
nome prima di eseguirlo: i costruttori scrivono anche owner_id, profile_id e
created_by. La riga to authenticated significa che la policy non viene
nemmeno presa in considerazione per un visitatore senza accesso, ed è quello che
tiene la tabella chiusa al pubblico.
Righe che chiunque può leggere. Un catalogo prodotti, articoli pubblicati, una mappa di locali.
create policy "anyone may read products"
on public.products for select
to anon, authenticated
using ( true );
Scrivi questa di proposito oppure non scriverla, perché è anche la policy a cui un costruttore di IA si aggrappa nel momento in cui gli chiedi di sistemare una schermata vuota. La domanda da risolvere prima: questa tabella potrebbe essere una pagina del tuo sito, esattamente com'è, senza togliere niente? Un no vuol dire che vuole la policy di proprietà qui sopra.
Righe che la tua app scrive. Leggere e scrivere sono permessi separati in Postgres, quindi una tabella su cui la tua app salva ha bisogno di una seconda policy, e questa controlla la riga in entrata e non in uscita.
create policy "insert own orders"
on public.orders for insert
to authenticated
with check ( (select auth.uid()) = user_id );
Quale clausola va dove è la parte che frega, e la riga update porta un
requisito che non ha nulla di ovvio:
| Operazione | Clausola | Richiede anche |
|---|---|---|
select | using | |
insert | with check | |
update | using e with check | una policy select sulla stessa tabella |
delete | using |
La documentazione di Supabase è esplicita sull'ultima: senza una policy di
select corrispondente, un update non funziona come ti aspetti. E se il messaggio
new row violates row-level security policy è quello che ti ha messo a cercare,
la distanza fra quelle due clausole è
tutto quell'errore.
Perché ogni esempio scrive (select auth.uid()) invece di auth.uid()
Perché le parentesi fanno calcolare il valore a Postgres una volta per tutta la query invece di una volta per ogni riga che guarda.
La versione avvolta diventa quello che Postgres chiama un initPlan: eseguito una sola volta e poi riusato per il resto dell'istruzione. Senza le parentesi la funzione viene richiamata alla riga uno, alla riga due, alla riga tre, e giù così per una tabella che potrebbe contenerne centomila. Il Performance Advisor di Supabase segnala la versione senza parentesi sotto una regola chiamata Auth RLS Initialization Plan, ed è una delle voci che la gente ci trova più spesso.
Un'avvertenza, ed è di Supabase stessa: funziona perché la risposta non cambia da una riga all'altra. Una funzione il cui risultato dipende davvero dalla riga che ha davanti non si può tirare fuori dal ciclo, quindi quella lasciala senza parentesi.
Già che ci sei, aggiungi un indice sulla colonna su cui filtrano le tue policy. La policy diventa una condizione a ogni lettura di quella tabella, quindi su una tabella con tante righe una colonna senza indice si vede nei tuoi tempi di risposta:
create index orders_user_id_idx on public.orders (user_id);
Testare una policy senza fabbricare un account finto
Il SQL Editor di Supabase può eseguire una query come se l'avesse mandata un visitatore preciso, e questo copre del tutto il caso anonimo.
L'editor porta un controllo per il ruolo con cui una query deve girare. Mettilo sul ruolo anonimo, poi lancia un normale select sulla tabella che hai appena cambiato. Quello che torna è quello che riceve uno sconosciuto senza accesso. Per un visitatore con accesso prendi l'id di un utente che hai già invece di crearne uno nuovo; qualsiasi riga nella tua tabella basta per la prova.
Se preferisci digitare invece che cliccare, la stessa cosa in SQL:
begin;
set local role anon;
select * from public.orders;
rollback;
begin e rollback stanno lì perché il cambio di ruolo duri quel blocco e non
di più, cosa che conta se stai passando in rassegna più tabelle di seguito.
Quello che questo ti dice è cosa fanno le tue policy dentro il database. Quello che non può dirti è cosa il tuo progetto consegna a internet, perché la richiesta che i tuoi visitatori fanno davvero non parte dal SQL Editor. Parte da un browser, porta una chiave pubblicabile e arriva attraverso l'indirizzo pubblico del tuo progetto.
Come testo l'RLS di Supabase da fuori la mia app?
Manda la richiesta che manderebbe uno sconosciuto. Ti servono due valori ed entrambi stanno già nel codice che il tuo sito serve a ogni visitatore: l'URL del tuo progetto e la tua chiave pubblicabile.
curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Authorization: Bearer YOUR_PUBLISHABLE_KEY"
Sostituisci un nome di tabella alla volta e leggi cosa torna. Un elenco vuoto,
[], vuol dire che la policy ha tenuto per un visitatore senza accesso. Una riga
vuol dire che chiunque abbia una chiave che viaggia con la tua app può leggere
quella tabella. Un errore che nomina la chiave stessa vuol dire che hai copiato
il valore sbagliato, e conviene escluderlo prima di concludere qualsiasi cosa.
Su un progetto Supabase recente quella chiave inizia con sb_publishable_, e su
uno più vecchio è la chiave anon. Entrambe si possono usare così senza rischio
e stare senza rischio nella tua app, è il loro scopo;
quali chiavi API stanno in un frontend
copre la coppia che non ci sta, e
dove trovarle nella dashboard sono i
quattro valori su quella pagina di impostazioni.
Questa è la prova che somiglia alla realtà, e averla passata su larga scala è il motivo per cui sappiamo quanto è diffuso il divario. Fra il 12 e il 14 agosto 2026 abbiamo passato nove controlli esterni su 30.998 app in produzione pubblicate da Lovable, Base44, Replit, v0 e Bolt. Delle 3.680 app con Supabase in cui il controllo è arrivato in fondo, 2.096 hanno risposto a una richiesta anonima con righe da almeno una tabella. Fa 57%, ed è una quota delle app che ci hanno dato una risposta netta, non di tutto quello che abbiamo scansionato. Il set di dati completo è pubblicato, e di cosa è una quota quel 57% ripercorre il conteggio.
Farlo a mano va bene con quattro tabelle e diventa noioso con quaranta. La nostra scansione gratuita manda quella richiesta al posto tuo, scopre quali tabelle esistono senza che tu le nomini e ti dice quali hanno risposto, accanto ad altri otto controlli che fa da fuori. Legge il tuo sito in produzione come può farlo qualsiasi visitatore, ci mette circa 20 secondi e non richiede un account: scansiona la tua app.
Prima di riscrivere policy su una tabella in produzione
Prendi prima una copia del tuo database. Stai per cambiare i permessi su ogni tabella che hai, con gli stessi strumenti che hanno prodotto il problema.
Vale la pena separare due rischi diversi, perché solo uno riguarda il lavoro di oggi. Se una tabella era leggibile e scrivibile da chiunque, stringerla adesso non cambia nulla di quello che è già successo, e quella versione salta fuori di solito come un messaggio di assistenza su dati che sono cambiati da soli. L'altro rischio è la migrazione in sé. Una policy cancellata e ricreata un po' storta è un martedì qualunque, e la via del ritorno è una copia di com'erano le cose un'ora fa.
Su un piano Supabase a pagamento la copia di stanotte aspetta nella console. Sul piano gratuito non c'è proprio nulla su cui ripiegare, perché Supabase non fa nessun backup automatico sul piano gratuito. Se è il tuo caso, prendine uno prima di iniziare.
Reeve Care è la versione di tutto questo che non devi ricordarti. Il tuo database Supabase viene copiato secondo un calendario, tenuto fuori dal tuo account Supabase, cifrato, e riletto per confermare che si ripristina prima che la data sulla tua dashboard si muova. I file caricati dai tuoi utenti viaggiano con lui appena colleghi una chiave Storage, e quella chiave si chiede a parte perché Supabase non emette nessuna chiave in sola lettura per i file: quella che copia i tuoi caricamenti può anche scrivere, quella che copia il tuo database no. Collegarla è facoltativo e il tuo database viene salvato in ogni caso. I backup sono solo di Supabase, quindi se i tuoi dati stanno altrove lo diciamo invece di venderti un abbonamento che sorveglia una scatola vuota.
Il ripristino è la parte che conta in un giorno come questo. Care copia lo stato attuale del tuo database prima di rigiocare la versione che hai scelto, quindi premere il pulsante ha un annulla tutto suo.
L'altra metà è il controllo che hai appena fatto a mano. Una policy che si allenta durante una migrazione successiva non è una cosa che si trova guardandola, quindi Care rilancia la stessa richiesta anonima secondo un calendario e ti dice quando una tabella comincia a rispondere mentre la settimana scorsa era zitta. Il monitoraggio dell'uptime e un report mensile stanno nello stesso abbonamento. Niente di tutto questo scrive le tue policy al posto tuo, e nessun backup chiude una tabella aperta. Quello che cambia è quanto ti costa una migrazione andata storta. Cosa Reeve salva su Supabase, ogni quanto, e cosa fa un ripristino percorre tutto il ciclo, e i piani sono sulla pagina prezzi.
L'ordine in cui lavorare
Cosa fare
- Elenca le tue tabelle con
select tablename, rowsecurity from pg_tables where schemaname = 'public'e guarda quante sono aperte prima di cambiare qualsiasi cosa. - Prendi un backup, poi attiva Row Level Security su ogni tabella dello schema public. Aspettati che la tua app diventi bianca; è il flag che lavora.
- Dai a ogni tabella una delle tre policy. La proprietà è il caso normale;
USING (true)è una decisione che prendi tabella per tabella, non un modo per riavere le schermate. - Scrivi
(select auth.uid())invece diauth.uid(), e indicizza la colonna su cui filtrano le tue policy. - Prova ogni tabella da fuori e senza accesso. Un elenco vuoto è promosso. Una riga è una tabella che chiunque può leggere, qualunque cosa dica la tua dashboard.
Comincia dalla tabella che ti imbarazzerebbe di più come pagina pubblica, e scendi da lì. La checklist di sicurezza in 10 minuti copre questo insieme al resto di quello che vale la pena confermare in un'app appena lanciata, e la guida alla sicurezza di Supabase passa in rassegna cos'altro tende a restare aperto.
FAQ
Cosa succede se attivo RLS e non scrivo nessuna policy?
La tabella smette di rispondere, anche alla tua app. Con il flag attivo, Postgres consulta le tue policy prima di consegnare una riga, e senza policy non c'è nulla che possa dire di sì, quindi le richieste tornano vuote. Supabase lo documenta direttamente: i dati diventano irraggiungibili attraverso l'API con una chiave pubblicabile finché non vengono definite delle policy. Non viene cancellato nulla e non si rompe nulla. Scrivi una policy per le righe che la tua app deve mostrare e le schermate tornano.
Row Level Security rallenta le mie query?
Può farlo, e le due correzioni sono piccole. Scrivi `(select auth.uid())` nella condizione della policy, con le parentesi, così Postgres calcola il valore una volta per tutta la query e poi lo riusa su ogni riga. Supabase segnala la versione senza parentesi nel suo Performance Advisor, sotto una regola chiamata Auth RLS Initialization Plan. Poi aggiungi un indice sulla colonna su cui filtra la policy, di solito `user_id`. Su una tabella da qualche migliaio di righe difficilmente noterai la differenza. Su una grande contano entrambe.
Mi serve RLS se la mia tabella non contiene dati personali?
Lo vuoi attivo lo stesso, e la policy può essere quella permissiva. Una tabella con Row Level Security spento è leggibile da chiunque abbia la chiave pubblicabile che viaggia con la tua app, il che va bene per un catalogo prodotti e molto meno bene per qualsiasi cosa non pubblicheresti come pagina. Attivarlo e scrivere una policy di lettura con `USING (true)` ti dà lo stesso accesso pubblico, ma di proposito, e la tabella si legge come decisa invece che come dimenticata la prossima volta che qualcuno ripassa l'elenco.
Perché la mia sottoscrizione realtime ha smesso di funzionare dopo aver attivato RLS?
Perché Realtime consulta le stesse policy prima di mandare una modifica a chi è iscritto. Una tabella con Row Level Security attivo e senza policy di select per quel visitatore non consegna righe a una query né modifiche a una sottoscrizione, esattamente per lo stesso motivo. Aggiungi la policy di select che serve a quella sottoscrizione e il flusso riprende. Se usi Realtime broadcast o presence invece delle modifiche al database, quelli sono autorizzati a parte, tramite policy scritte sulla tabella `realtime.messages`.
Come faccio a testare una policy senza creare un utente finto?
In due modi, e nessuno dei due richiede un account nuovo. Il SQL Editor di Supabase può eseguire una query come se l'avesse mandata un ruolo preciso, e questo copre del tutto il caso anonimo: quello che torna è quello che riceve uno sconosciuto. Per il caso con login serve un id utente che faccia da controfigura, e va bene qualsiasi riga già presente nella tua tabella. Il secondo modo è una richiesta da fuori che porta la tua chiave pubblicabile, e verifica tutto il percorso e non solo la policy.