Vai al contenuto

Basi della sicurezza

Supabase "permission denied for schema public": tre cause

Supabase "permission denied for schema public" indica un grant mancante sullo schema. Cosa lo ha tolto, e la riga che lo rimette.

Vlad Tkachenko12 min di lettura
Un cortile recintato di tabelle di database, ogni porta aperta e illuminata, dietro un cancello d'ingresso chiuso con le sbarre abbassate.

In breve

  • Supabase "permission denied for schema public" significa che il ruolo che fa la richiesta non ha alcun diritto di entrare nello schema public, quindi Postgres ha rifiutato prima ancora di guardare una sola tabella.
  • Ogni progetto Supabase parte con quel diritto concesso ad anon, authenticated e service_role. Se la tua app riceve questo errore, qualcosa lo ha tolto: lo schema è stato cancellato e ricreato, oppure uno script ha revocato il grant.
  • Rimetterlo è una riga di SQL. Il grant lascia solo arrivare un ruolo alle tue tabelle, e Row Level Security continua a decidere quali righe riceve ogni visitatore, quindi controlla quelle regole prima di considerarlo risolto.

Ieri la tua app funzionava. Oggi ogni schermata che legge da Supabase resta vuota o mostra un errore, e la console del browser ha la stessa riga per ciascuna: permission denied for schema public. Apri la dashboard di Supabase e tutte le tabelle sono ancora lì. Il SQL Editor le legge senza un lamento.

La risposta dietro quella riga arriva di solito come 401 o 403. Ridotta ai due campi che contano, dice questo:

{
  "code": "42501",
  "message": "permission denied for schema public"
}

Supabase "permission denied for schema public" viene da Postgres, il database su cui gira Supabase, e l'elenco delle sue cause è corto. Ogni progetto Supabase viene creato con un grant che fa entrare la tua app nello schema public. Qualcuno, o qualcosa, lo ha tolto, e rimetterlo è una riga di SQL.

Ecco la parte che è facile capire al contrario: quel grant è la porta d'ingresso dell'edificio, e le serrature dei tuoi dati stanno più all'interno. Se qualcuno lo ha tolto per tenere fuori gli estranei, ha tenuto fuori anche la tua app. E riaprirla è sicuro solo se quelle serrature interne reggono, quindi sono loro da controllare prima di considerarlo risolto.

Cosa significa "permission denied for schema public"?

Significa che il ruolo che fa la richiesta non ha il diritto di entrare nello schema public, quindi Postgres ha rifiutato prima di guardare una sola tabella.

Uno schema è una cartella di tabelle con un nome, dentro il tuo database. public è quella in cui Lovable, Bolt, v0, Cursor, Claude Code e Replit mettono le tue tabelle se nessuno dice loro altro. Un ruolo è il tipo di visitatore da cui arriva una richiesta: anon è qualcuno che non ha fatto l'accesso, authenticated qualcuno che l'ha fatto, e service_role il tuo codice server che usa la chiave segreta. Un grant dà a un ruolo un permesso su una cosa.

Il permesso di cui parla questo errore si chiama USAGE, e la documentazione di Postgres lo descrive come il diritto di cercare ("look up") gli oggetti dentro lo schema. Un ruolo senza questo permesso non raggiunge nessuna tabella di quello schema, qualunque cosa dicano le impostazioni della tabella stessa.

Se la versione per tabella di questo errore ti ha abituato a vedere ogni tabella come una stanza con una porta per ogni ruolo, lo schema è l'edificio in cui stanno quelle stanze, e USAGE è la sua porta d'ingresso. permission denied for schema public significa che la tua richiesta è ferma fuori da quella porta. Quali stanze abbiano la porta aperta non cambia nulla, perché la richiesta non arriva mai fino a una stanza.

Supabase dà quella porta d'ingresso a tutti e tre i ruoli quando crea un progetto. L'immagine di database di Supabase esegue questa riga nel suo primo script di configurazione:

grant usage on schema public to postgres, anon, authenticated, service_role;

Quindi se la tua app riceve questo errore, il grant c'era e adesso non c'è più.

Perché il SQL Editor funziona ancora?

Perché il SQL Editor non usa nessuno dei ruoli della tua app. Una guida alla risoluzione dei problemi di Supabase dice che una query lanciata da lì è "routed through the postgres role at the database level", cioè passa per il ruolo postgres a livello di database.

postgres è il ruolo proprietario delle tue tabelle, e se lo schema stesso è stato ricreato dalla dashboard o dal tuo builder, è proprietario anche di quello. Un proprietario non ha bisogno di alcun grant per entrare nel proprio edificio. Così la dashboard mostra ogni tabella, un select nel SQL Editor restituisce ogni riga, e la stessa richiesta dalla tua app viene respinta alla porta d'ingresso. Niente su quelle schermate ti dice che i ruoli della tua app hanno perso la strada per entrare.

Per vedere quello che vede la tua app, chiedi a Postgres direttamente dei suoi ruoli. Incolla questo nel SQL Editor:

select rolname,
       has_schema_privilege(rolname, 'public', 'USAGE') as can_enter
from pg_roles
where rolname in ('anon', 'authenticated', 'service_role');

Ogni riga dovrebbe dire true. Una riga che dice false è un ruolo che la tua app usa e che non riesce più a entrare.

Il grant apre l'edificio, e le serrature sono dentro

USAGE lascia arrivare un ruolo alle tabelle di uno schema. Non dice nulla su quali di quelle tabelle il ruolo possa usare, né su quali righe riceva.

Dietro la porta d'ingresso ci sono altri due controlli, ed entrambi appartengono a ciascuna tabella. Il grant proprio della tabella decide se un ruolo può usare quella tabella, e quando manca è ciò che segnala permission denied for table. Poi Row Level Security, le regole per riga che forse conosci già, decide quali righe riceve quel ruolo. Postgres fa le tre domande in quest'ordine e si ferma al primo rifiuto.

Postgres controlla lo schema, poi la tabella, poi le righe, e si ferma al primo rifiuto. Un cancello chiuso respinge la richiesta prima ancora che si provi la porta di una qualsiasi tabella.

È proprio quest'ordine a fare del grant sullo schema una pessima serratura. Per un ruolo è tutto o niente: toglilo ad anon e ogni schermata che vede un visitatore non autenticato smette di funzionare tutta insieme, compresa una pagina di accesso che legge qualcosa dalle tue tabelle. Toglilo ad authenticated e succede lo stesso a tutti quelli che hanno fatto l'accesso. La documentazione di Postgres aggiunge che revocarlo "is not a completely secure way to prevent object access", cioè non è un modo del tutto sicuro di impedire l'accesso, e che un ruolo senza questo permesso può ancora leggere i nomi delle tue tabelle nei cataloghi di sistema.

Se vedi questo errore, la tua app ha bisogno del ruolo che è stato respinto.

Causa 1: lo schema è stato cancellato e ricreato

Questa è la causa che si porta via di più. Qualcuno ha eseguito queste due righe, o qualcosa che fa la stessa cosa:

drop schema public cascade;
create schema public;

La prima riga elimina lo schema e, a causa di cascade, ogni tabella, vista e funzione che c'era dentro. La seconda crea un public nuovo e vuoto. Uno schema nuovo appartiene al ruolo che lo ha creato, e la documentazione di Postgres dice chiaramente che nessun altro riceve per impostazione predefinita alcun privilegio su uno schema. Il grant che Supabase ha eseguito quando ha creato il tuo progetto apparteneva al vecchio schema e se n'è andato con lui, e Supabase non esegue una seconda volta il suo script di configurazione.

Quelle due righe sono il modo più breve per svuotare un database Postgres, e per questo sono un passo che un assistente IA può fare quando gli chiedi di ripartire da zero con il database o di far passare pulita una migrazione che fallisce.

Dopo, le tue tabelle tornano se le migrazioni dell'app vengono eseguite di nuovo. Una migrazione è uno dei file di modifiche al database che scrive il tuo builder, ed eseguirle tutte ricostruisce le tabelle che descrivono. Quello che non torna è tutto ciò che non nominano mai, e il grant della porta d'ingresso lo ha creato Supabase prima che esistesse la tua prima migrazione. Le migrazioni scritte dal tuo builder potrebbero non contenerlo affatto.

Altre due cose se ne vanno con il vecchio schema:

  • I grant automatici per le nuove tabelle. Postgres li conserva come privilegi predefiniti (default privileges) dello schema, e vengono cancellati insieme a lui. Le tabelle create dopo partono senza grant per nessuno dei ruoli della tua app.
  • Le righe. Le migrazioni ricostruiscono tabelle vuote. Quello che c'era nelle vecchie torna solo se qualcuno ha ripristinato una copia fatta prima del reset.
Prima del reset tutti e quattro i ruoli potevano entrare nello schema. Dopo, le tabelle si possono ricostruire, ma il cancello e le loro porte si aprono solo per postgres, che le ha costruite.

Se le tue tabelle sono tornate vuote, trovare quella copia viene prima di qualsiasi altra cosa in questa pagina.

Causa 2: un revoke pensato per proteggere

Il secondo modo è voluto. Qualcuno ha letto che ogni tabella in public è raggiungibile tramite la Data API di Supabase, l'indirizzo web attraverso cui la tua app legge e scrive i suoi dati, e ha revocato il grant sullo schema per chiuderla.

La preoccupazione di fondo è ragionevole, e lo strumento scelto è quello sbagliato. La guida di Supabase per proteggere la Data API non tocca mai il grant sullo schema. Quello che revoca è l'accesso automatico che ricevono le nuove tabelle di public, così ogni nuova tabella resta chiusa finché qualcuno non le dà accesso di proposito. Supabase lo attiva per tutti i progetti esistenti il 30 ottobre 2026.

Se uno script che hai eseguito, o un assistente a cui hai chiesto di rinforzare il database, ha tolto il grant sullo schema, la query di controllo qui sopra mostra quali ruoli lo hanno perso. Ridallo a quelli che usa la tua app, poi proteggi le cose come descrivono le sezioni qui sotto.

Causa 3: l'errore arriva da una migrazione

Quando il messaggio compare mentre qualcosa sta creando tabelle, per esempio una migrazione, un deploy, oppure Prisma o Drizzle che preparano il tuo database, il permesso che manca è un altro. Creare qualsiasi cosa in uno schema richiede CREATE su di esso, e il testo dell'errore quando manca CREATE è la stessa riga.

Da Postgres 15 i ruoli ordinari non ricevono più CREATE su public per impostazione predefinita. La documentazione di Postgres dice che solo i database aggiornati dalla versione 14 o precedenti lo danno ancora a tutti. Così un utente di database a parte, creato per uno strumento di migrazione, viene respinto già alla sua primissima tabella. La guida a Prisma di Supabase crea un utente prisma, e la sua configurazione include grant create on schema public to prisma.

La soluzione è un grant create a quell'unico utente delle migrazioni. Nessuno che visita la tua app ha bisogno di creare tabelle, quindi anon e authenticated non ne ricevono mai uno.

Come rimettere il grant in sicurezza

Esegui una riga, verifica che tutti e tre i ruoli possano entrare, poi ricarica la tua app e leggi cosa dice dopo.

La riga, nel SQL Editor:

grant usage on schema public to anon, authenticated, service_role;

Resta su usage. Anche grant all on schema public fa sparire l'errore, e su uno schema all vuol dire usage più create, il diritto di aggiungere tabelle proprie. Supabase consiglia di dare a ogni ruolo il minimo di cui ha bisogno, e ai ruoli dei tuoi visitatori serve solo entrare.

La query di controllo di prima dovrebbe ora dire true per tutti e tre i ruoli. Ricarica la tua app, e succede una di due cose:

  • Funziona. La porta d'ingresso era l'unica cosa che mancava.
  • Un nuovo messaggio nomina una tabella. permission denied for table significa che manca anche il grant proprio di quella tabella, cosa da aspettarsi dopo la prima causa, perché le tabelle ricostruite non hanno mai ricevuto i grant automatici. Dai a ogni tabella il suo grant, Row Level Security e una regola in un'unica modifica.

Resisti alla tentazione di dare a ogni ruolo tutto su ogni tabella per far sparire il secondo messaggio. Una tabella con un grant ad anon e Row Level Security disattivata consegna tutte le sue righe a chiunque le chieda, e nella tua app non si vedrà nulla di diverso.

Se il messaggio nomina un altro schema, come permission denied for schema api, quello schema lo ha creato qualcuno, e non ha mai ricevuto i grant con cui parte public. La guida di Supabase per usare schemi personalizzati elenca le righe che gli servono.

Cosa proteggere invece

I controlli che tengono gli estranei lontani dai tuoi dati appartengono a ciascuna tabella, e la guida alla sicurezza di Supabase li nomina:

  1. Un grant per tabella, per i ruoli che ne hanno bisogno. Una tabella di ordini non ha bisogno di alcun grant per anon.
  2. Row Level Security su ogni tabella, con una regola.Come attivarla, e controllare che regga.
  3. Nuove tabelle chiuse per impostazione predefinita. Revocare i grant automatici lo fa già oggi, e il 30 ottobre lo fa per te.
  4. Le tabelle interne fuori dalla Data API. La guida di Supabase descrive come mettere ciò che legge la tua app in uno schema dedicato, così tutto il resto sta in schemi che la Data API non serve.

Poi guarda il risultato da fuori, perché nessuna di quelle schermate mostra cosa riceve un estraneo. La nostra scansione gratuita chiede a ogni tabella che la tua app nomina quante righe riceverebbe un visitatore non autenticato, legge il numero senza scaricare nemmeno una riga e richiede circa 20 secondi, senza account: scansiona la tua app.

Tenere le tue tabelle, e ciò che c'è dentro

Dopo oggi possono ancora andare storte due cose: una modifica successiva apre una tabella agli estranei, e un reset come quello della prima causa si porta via le tue righe.

Reeve Monitor continua a controllare da fuori:

  • il controllo di sicurezza completo ogni ora, su un massimo di tre app
  • un messaggio quando un risultato cambia, così una tabella che si è aperta dopo una migrazione non aspetta che tu vada a guardare
  • se la tua app risponde
  • un report mensile, in linguaggio semplice, di ciò che ha visto

Reeve Care tiene una copia di ciò che c'è nelle tue tabelle:

  • una copia cifrata del tuo database Supabase secondo una pianificazione, sullo storage di Reeve nell'UE, fuori dal tuo account Supabase
  • ogni copia verificata prima che conti, contando le righe di ogni tabella
  • anche i file caricati, appena colleghi una credenziale di Storage
  • un ripristino con un clic, con una copia di sicurezza dello stato attuale fatta prima
  • tutto ciò che fa Monitor

Cosa fare adesso

Cosa fare

  • Esegui la query di controllo nel SQL Editor. Ogni ruolo che dice false è un ruolo che la tua app ha perso.
  • Rimetti il grant con grant usage on schema public to anon, authenticated, service_role; e resta su usage.
  • Se lo schema è stato cancellato, guarda se le tue tabelle sono vuote e cerca una copia fatta prima del reset.
  • Se poi compare un errore di tabella, dai a quella tabella il suo grant, Row Level Security e una regola insieme.
  • Proteggi tabella per tabella, dove si può regolare, e lascia stare il grant sullo schema.
  • Se l'errore veniva da una migrazione, dai create all'utente delle migrazioni e a nessun altro.

Comincia dalla query di controllo, perché un solo risultato ti dice quali ruoli della tua app hanno perso la strada per entrare. Poi, con la porta di nuovo aperta, scansiona la tua app per vedere cosa consegna a un estraneo.

FAQ

Cosa significa "permission denied for schema public" in Supabase?

Significa che il ruolo che fa la richiesta, di solito anon o authenticated, non ha il diritto di entrare nello schema public, quindi Postgres ha rifiutato prima di guardare qualsiasi tabella. Porta il codice 42501 e di solito arriva alla tua app come 401 o 403. Ogni progetto Supabase parte con quel diritto concesso, quindi l'errore significa che qualcuno o qualcosa lo ha tolto.

È sicuro concedere usage sullo schema public?

Sì. Ogni progetto Supabase parte proprio con quel grant. Lascia solo arrivare un ruolo alle tabelle dello schema: ogni tabella ha comunque bisogno del suo grant, e Row Level Security continua a decidere quali righe riceve ogni visitatore. Quello che merita uno sguardo sono le tabelle dietro, perché una con un grant ad anon e Row Level Security disattivata consegna tutte le sue righe a chiunque le chieda.

Che differenza c'è con "permission denied for table"?

L'errore sullo schema ferma la richiesta prima che arrivi a qualsiasi tabella, quindi tutte le tabelle dello schema falliscono insieme. L'errore sulla tabella riguarda una sola tabella che non ha un grant per quel ruolo, e il resto della tua app continua a funzionare. Entrambi portano il codice 42501, quindi a distinguerli sono le parole del messaggio.

Perché le mie tabelle funzionano nel SQL Editor di Supabase ma non nella mia app?

Perché il SQL Editor esegue le tue query con il ruolo postgres, che è il proprietario delle tue tabelle e non ha bisogno del grant che i ruoli della tua app hanno perso. La tua app usa anon per i visitatori non autenticati e authenticated per quelli che hanno fatto l'accesso. Chiedi a Postgres direttamente di quei due ruoli, con has_schema_privilege, per vedere quello che vede la tua app.

Cosa dovrei proteggere al posto del grant sullo schema?

Le tabelle. Dai a ogni tabella un grant solo per i ruoli che ne hanno bisogno, attiva Row Level Security con una regola su ogni tabella e lascia che le nuove tabelle partano senza alcun accesso. Supabase rende quest'ultima parte l'impostazione predefinita per i progetti esistenti il 30 ottobre 2026.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Fai il backup del database Supabase prima della prossima modifica

Reeve ne tiene una copia cifrata a intervalli programmati e la ripristina per te se qualcosa va storto.

Vedi i backup Supabase

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.