Supabase ti dà un database e un'API in pochi minuti, ed è proprio questo il pericolo: l'API è esposta fin dal primo momento. Se non configuri la sicurezza, chiunque abbia la tua chiave pubblica può leggere e scrivere nelle tue tabelle. Questa guida ripassa, nell'ordine in cui conviene farlo, tutto quello che va controllato prima di aprire la tua app al mondo: RLS e policy con SQL d'esempio, quale chiave va dove, lo Storage, le viste, la configurazione di Auth e come verificare alla fine che sia davvero tutto chiuso.
Come Supabase espone i tuoi dati (e perché è una cosa buona)
Davanti al tuo Postgres c'è uno strato (PostgREST) che trasforma ogni tabella dello schema public in un endpoint HTTP. La tua app nel browser gli parla direttamente con una chiave pubblica. Questo ti risparmia di scrivere un backend, ma significa che nessun server tuo sta in mezzo a controllare i permessi: le policy di Row Level Security sono il tuo backend. Se sono giuste, il design è solido; se mancano, l'intera tabella è a un URL di distanza.
1. RLS attivo su TUTTE le tabelle
Il Row Level Security è la barriera principale di Supabase. Senza, la chiave pubblica può interrogare intere tabelle direttamente dal browser. L'errore classico del vibe coder è creare una tabella nuova di fretta, o lasciare che la crei l'IA, e dimenticarsi di attivare l'RLS. È stata esattamente la causa della vulnerabilità di Lovable (CVE-2025-48757).
Controlla tabella per tabella nel Table Editor: quelle con "RLS disabled" sono aperte al pubblico. Per non affidarti all'occhio, questa query nel SQL Editor elenca quelle che mancano:
select c.relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;
E per ciascuna:
alter table public.nome_tabella enable row level security;
Attivarlo non basta: una tabella con RLS attivo ma senza policy nega tutto (la tua app smette di funzionare finché non le scrivi), e una con una policy troppo permissiva non protegge nulla.
2. Policy che riflettono davvero chi può fare cosa
Una policy using (true) per tutti lascia la tabella aperta anche con l'RLS attivo; l'IA le suggerisce spesso "per farlo funzionare". Scrivi policy precise e separa le regole per operazione, perché a volte vuoi permettere la lettura ma non la cancellazione.
Schema A: a ogni utente le sue righe
create policy "notes: leggi le proprie" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);
create policy "notes: crea le proprie" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "notes: modifica le proprie" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Il with check è ciò che impedisce a qualcuno di inserire o modificare una riga a nome di un altro utente. Avvolgere auth.uid() in un (select …) fa sì che Postgres lo valuti una volta per query e non una volta per riga; su tabelle grandi la differenza è enorme. Metti un indice su user_id.
Schema B: appartenenza a un team o a un'organizzazione
create policy "projects: membri del team" on public.projects
for select to authenticated
using (
exists (
select 1 from public.team_members m
where m.team_id = projects.team_id
and m.user_id = (select auth.uid())
)
);
Anche la tabella team_members ha bisogno delle sue policy, e conviene che solo un amministratore possa inserirvi righe.
Schema C: contenuto pubblico in sola lettura
create policy "posts: lettura pubblica" on public.posts
for select to anon, authenticated
using (published = true);
È legittimo. Quello che non deve esistere è una policy di scrittura aperta o colonne con dati personali in una tabella leggibile pubblicamente.
3. anon, service_role e le nuove chiavi
La chiave anon è pubblica e va nel frontend: è pensata per essere governata dall'RLS. La chiave service_role bypassa completamente l'RLS e ha accesso totale. Non deve mai, mai, comparire nel browser né in una variabile NEXT_PUBLIC_, VITE_ o NUXT_PUBLIC_: quel prefisso significa "questo viene inviato al client".
Supabase ha inoltre introdotto una nuova coppia di chiavi con nomi che non lasciano dubbi: sb_publishable_… (per il browser, soggetta all'RLS) e sb_secret_… (solo server, bypassa l'RLS, revocabile separatamente). Se inizi un progetto oggi, usa queste; se ne hai uno vecchio, migra appena puoi. Usa la chiave segreta solo lato server, in Edge Functions o backend fidati, e se sospetti una fuga, ruotala subito dalla dashboard.
4. Storage: anche i bucket hanno regole
L'archiviazione dei file usa policy proprie, sulla tabella storage.objects. Un bucket pubblico serve qualsiasi file via URL a chiunque la possieda, per sempre. Decidi consapevolmente quali bucket sono pubblici (avatar, immagini prodotto) e proteggi il resto. Un esempio perché ogni utente legga e carichi solo nella propria cartella:
create policy "documents: cartella personale" on storage.objects
for all to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
)
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
Carica i documenti privati in bucket privati e servili con URL firmati di breve durata, non in un bucket pubblico con nomi difficili da indovinare.
5. Viste, funzioni e schemi esposti
Tre punti da cui i dati scappano anche quando le tabelle sono a posto:
- Le viste. In Postgres una vista gira con i permessi di chi l'ha creata, quindi una vista su una tabella protetta può restituire righe che le policy vieterebbero. Creale con
with (security_invoker = true)(Postgres 15 o successivo) o controllale una per una. - Le funzioni RPC. Una funzione
security definergira con i permessi del suo proprietario. Usala solo quando ti serve davvero, valida i parametri al suo interno e limita chi può chiamarla conrevoke execute … from anon. - Gli schemi esposti. In Settings → API scegli quali schemi pubblica l'API. Quello che non deve uscire via HTTP non deve stare in quella lista.
6. Impostazioni di Auth da controllare
- Conferma email attiva, così nessuno registra account con l'indirizzo di altri.
- Limiti ai tentativi di login e all'invio di email: sono attivi di default; verifica che nessuno li abbia alzati "per testare".
- URL di redirect consentiti solo verso i tuoi domini; una lista aperta permette di usare il tuo login per reindirizzare a siti di phishing.
- Provider OAuth: solo quelli che usi, con la schermata di consenso a nome del tuo brand.
- Scadenza dei magic link e degli OTP breve; il valore di default è ragionevole, non allungarlo.
7. Segreti e chiavi fuori dal frontend
Oltre alla chiave segreta di Supabase, controlla che non ci siano chiavi di terze parti (Stripe, email, IA) incorporate nel JavaScript del client. L'IA a volte le mette lì per far funzionare qualcosa in fretta, e restano visibili per sempre nel bundle. Le chiamate ai servizi a pagamento passano da una Edge Function o dal tuo server, mai dal browser. Vedi i segreti che la tua app fa trapelare nel browser.
8. Ambienti: lo sviluppo non è la produzione
Usa un progetto Supabase separato per lo sviluppo, con dati inventati. Un progetto di test con una copia dei dati reali e policy fatte a metà è esposto quanto la produzione, e nessuno lo sorveglia.
Come verificare che sia chiuso (il test finale)
- Test in incognito. Con l'app aperta, copia da F12 → Network una chiamata a
/rest/v1/e aprila senza sessione. Deve restituire una lista vuota o un errore. - Test del "chiedere di più". Da loggato, togli il filtro dall'URL (
?user_id=eq.…). Devono tornare solo le tue righe. - Test dei due account. Crea due utenti e verifica che nessuno veda, modifichi o cancelli i dati dell'altro, file dello Storage compresi.
- Security Advisor. Nella dashboard, la sezione Advisors elenca tabelle senza RLS, policy permissive e funzioni esposte. Tienila verde.
Scanaris controlla il tuo Supabase per te
Scanaris include un audit opzionale di Supabase che, con una connection string in sola lettura, verifica tabella per tabella se l'RLS è attivo e se esistono policy, e nella scansione normale rileva chiavi esposte nel frontend, CORS permissivo ed endpoint che servono dati senza sessione. Ogni risultato arriva con un prompt pronto da incollare nella tua IA. Lancialo prima del lancio e saprai se hai lasciato una porta aperta; attiva il monitoraggio e ti avviserà se l'IA ne riapre una in una tabella nuova.
Domande frequenti
Posso saltare l'RLS mettendo davanti un mio backend?
Puoi, usando solo la chiave segreta dal tuo server e mai quella pubblica nel client. Ma allora il tuo backend deve fare tutti i controlli di permessi che farebbe l'RLS. Per la maggior parte delle app, l'RLS è meno lavoro e più difficile da rompere.
L'RLS penalizza le prestazioni?
Appena, se le policy sono scritte bene: (select auth.uid()) invece di auth.uid(), indici sulle colonne usate dalle policy e nessuna sottoquery pesante. Una policy scritta male può invece rallentare una tabella grande.
E le tabelle che l'IA crea dopo il lancio?
Nascono senza RLS. Aggiungi "attiva l'RLS e scrivi le policy" alla tua routine (o al prompt della tua IA) ogni volta che compare una tabella nuova, e usa una nuova scansione automatica per scoprire se è stato dimenticato.