Basi della sicurezza
Supabase "relation does not exist" quando la tabella c'è
Supabase "relation does not exist" o PGRST205: nessuna tabella ha quel nome in quello schema. Quattro controlli, e che cosa apre esporre uno schema.

In breve
- Supabase "relation does not exist" (codice Postgres 42P01) significa che negli schemi percorsi dalla query non c'è nessuna tabella né vista con esattamente quel nome. Dalla tua app, su un progetto recente, lo stesso problema si legge "Could not find the table in the schema cache" (PGRST205).
- Controlla prima il nome lettera per lettera, maiuscole comprese, poi lo schema, poi il progetto. Dentro una funzione con il search_path vuoto, ogni tabella ha bisogno del suo schema davanti: public.invoices.
- Esporre uno schema fa sparire l'errore e lascia che l'API raggiunga ogni tabella che contiene, quindi controlla la Row Level Security di ciascuna prima di farlo.
Il tuo builder ha aggiunto una tabella invoices. La vedi nel Table Editor, con
le sue righe, e la pagina che dovrebbe elencarle resta vuota. La console del
browser mostra questo:
{
"code": "PGRST205",
"details": null,
"hint": "Perhaps you meant the table 'public.Invoices'",
"message": "Could not find the table 'public.invoices' in the schema cache"
}
Supabase "relation does not exist" significa che Postgres non ha trovato nessuna
tabella né vista con esattamente quel nome negli schemi percorsi dalla query.
PGRST205 è la stessa risposta data da PostgREST, la parte di Supabase che
trasforma le chiamate supabase.from(...) della tua app in query al database,
prima ancora che la query arrivi a Postgres.
La soluzione che gira per questo errore è aggiungere lo schema a Exposed schemas nelle impostazioni del progetto. Risolve una delle quattro cause qui sotto. Lascia anche che chiunque abbia la chiave pubblica della tua app raggiunga ogni tabella di quello schema, quindi leggi che cosa apre prima di attivarla.
Che cosa significa "relation does not exist" in Supabase?
Il database ha cercato una tabella con esattamente quel nome, negli schemi che
doveva percorrere, e non ne ha trovata nessuna. "Relation" è la parola di Postgres
per qualunque cosa da cui escono righe, una tabella o una vista. Il codice
dell'errore è 42P01, che
Postgres chiama undefined_table.
Pensa al tuo database come a un edificio. Ogni schema è un piano: public è il
piano terra, e auth, dove Supabase tiene i tuoi utenti, è un altro. Ogni tabella
è una stanza con il suo nome sulla porta. Una query che dà solo il nome di una
stanza la cerca ai piani elencati nel suo percorso di ricerca. Le richieste della
tua app salgono con un ascensore, e l'ascensore si ferma solo ai piani che il tuo
progetto espone. L'errore significa che nessuno ha trovato una porta con quel nome
ai piani che ha controllato.
La formulazione che vedi dipende da dove è girata la query:
| Dove è girata la query | Che cosa torna |
|---|---|
| Il SQL Editor, una migrazione, una funzione o un trigger | relation "invoices" does not exist, codice 42P01 |
| La tua app, con PostgREST 13 o successivo | Could not find the table 'public.invoices' in the schema cache, codice PGRST205, HTTP 404 |
| La tua app, con un PostgREST più vecchio | una lettura riceve relation "public.invoices" does not exist, HTTP 404, e un insert un errore vuoto |
| La tua app, che chiede uno schema che il progetto non espone | Invalid schema: billing, codice PGRST106, HTTP 406 |
Il changelog di PostgREST
data la formulazione nuova alla versione 13.0.0, uscita l'8 maggio 2025:
"Querying non-existent table now returns PGRST205 error". Prima, come ha
spiegato uno dei suoi maintainer nell'issue #3697,
una lettura andava a Postgres senza controlli e tornava come 42P01. La
guida di Supabase alla protezione della Data API
dice che puoi controllare la versione di PostgREST del tuo progetto nella
Dashboard. Il codice di PostgREST
fissa il resto: il suggerimento di PGRST205, quando c'è, nomina la tabella più
simile che conosce sullo stesso piano.
Se invece il messaggio dice permission denied for table, la tabella è stata
trovata e la tua app è stata respinta alla porta. Quello ha
un'altra soluzione.
Causa 1: il nome sulla porta ha una maiuscola
Il nome deve coincidere esattamente, maiuscole comprese, e due degli strumenti che usi trattano le maiuscole in modo diverso dal terzo.
La documentazione di Postgres riassume la regola in una frase: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case." Gli altri due tengono quello che hai scritto:
- il Table Editor di Supabase crea la tabella con esattamente il nome che le dai.
Il suo codice
mette il nome tra virgolette quando servono, quindi
InvoicesrestaInvoices - PostgREST mette tra virgolette doppie ogni nome che la tua app invia
(
pgFmtIdent), quindi.from('invoices')chiedeinvoicese nient'altro
| Come è stata creata la tabella | In SQL, scrivi | Nella tua app, scrivi |
|---|---|---|
create table invoices, o Invoices scritto in SQL senza virgolette | invoices | .from('invoices') |
create table "Invoices", o Invoices scritto nel Table Editor | "Invoices", virgolette comprese | .from('Invoices') |
La seconda riga produce questo errore da entrambi i lati. La tua app chiama
.from('invoices') e riceve PGRST205, con il nome vero nel suggerimento, come nel
messaggio in cima a questa pagina. Oppure l'app funziona, e una funzione o una
policy che nomina Invoices senza virgolette fallisce con
relation "invoices" does not exist, perché Postgres ha portato il nome in
minuscolo prima di cercare.
Per vedere ogni tabella con quel nome, a qualunque piano e con qualunque combinazione di maiuscole, esegui questo nel SQL Editor:
select table_schema, table_name
from information_schema.tables
where table_name ilike 'invoices';
La soluzione che non tocca niente dei tuoi dati sta nel codice: scrivi
"Invoices" con le sue virgolette in SQL, e le maiuscole esatte nell'app.
Rinominare la tabella invece rompe ogni schermata che usa ancora il nome vecchio.
La versione per colonne di questo errore
ha la stessa trappola per i nomi delle colonne.
Causa 2: la tabella è a un altro piano
La tua app cerca in public se non le dici altro, quindi una tabella in
qualunque altro schema per lei non c'è. Per questo il messaggio dice
public.invoices: public è il piano dove è andato l'ascensore.
Succede in tre modi:
- il tuo builder ha creato la tabella in uno schema suo, come
billingoprivate, e l'app chiama un semplice.from('invoices') - lo schema è scritto dentro il nome, come
.from('billing.invoices'). PostgREST legge l'intera stringa come un solo nome di tabella e la cerca inpublic - la tabella è
auth.users. La guida di Supabase sui dati degli utenti dice: "For security, the Auth schema is not exposed in the auto-generated API." La sua risposta è una tabella tua inpublic,profilesnel suo esempio, riempita da un trigger quando qualcuno si registra, e quel trigger può fallire a modo suo
Una tabella a un altro piano si legge con .schema('billing').from('invoices'), e
quello schema deve comparire in Exposed schemas nelle impostazioni API del
progetto, come mostra la
guida di Supabase sugli schemi personalizzati.
Finché non c'è, la risposta diventa PGRST106:
{
"code": "PGRST106",
"details": null,
"hint": "Only the following schemas are exposed: public, graphql_public",
"message": "Invalid schema: billing"
}
Il suggerimento elenca i piani a cui l'ascensore si ferma davvero. PostgREST 12
formula lo stesso errore in un altro modo, con l'elenco dentro il messaggio:
The schema must be one of the following: public, graphql_public. È il testo che
produce il suo codice per quella versione,
e il changelog ha spostato il nome dello schema nel messaggio con la 13.0.7.
La tua chiave segreta sale con lo stesso ascensore. L'esposizione è
un'impostazione dell'API, quindi il codice server che usa supabase-js con quella
chiave riceve lo stesso PGRST106.
Che cosa apre esporre uno schema?
L'ascensore comincia a fermarsi a quel piano, e con i permessi che dà la guida di Supabase ogni visitatore riceve una chiave di ogni stanza che c'è. La Row Level Security è allora l'unica cosa che decide che cosa ciascuno può leggere o cambiare.
La guida sugli schemi personalizzati ha due passaggi. Il primo è l'impostazione Exposed schemas. Il secondo è un blocco di SQL, e le sue prime due righe sono:
GRANT USAGE ON SCHEMA myschema TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA myschema TO anon, authenticated, service_role;
anon è il ruolo dietro ogni richiesta di chi non ha fatto l'accesso. ALL
significa leggere, aggiungere, cambiare e cancellare. Il blocco continua con
ALTER DEFAULT PRIVILEGES, che dà lo stesso accesso a ogni tabella creata in
seguito a quel piano.
È sicuro dove la Row Level Security, le regole per riga che Supabase controlla a ogni richiesta, è attivata e scritta per ogni tabella. La guida di Supabase alla protezione della Data API dice: "Tables created through the Supabase Dashboard have RLS enabled by default. Enable RLS explicitly for tables created in the SQL Editor or through another tool." Uno schema che il tuo builder ha preparato in SQL può contenere tabelle dove è spenta. Esposta con quei permessi, una tabella così consegna ogni riga a chiunque abbia la tua chiave pubblica, e accetta anche le sue modifiche.
Controlla prima di esporre:
select tablename, rowsecurity
from pg_tables
where schemaname = 'billing';
Ogni riga che dice false è una tabella senza nessuna regola.
Attivare la Row Level Security su ogni tabella
spiega come attivarla e scrivere la prima regola, e
"RLS disabled in public" spiega la regola
di lettura aperta che lascia una tabella altrettanto raggiungibile anche con la Row
Level Security attiva.
Puoi anche dare meno permessi della guida. Un piano con una tabella che leggono solo gli utenti che hanno fatto l'accesso ha bisogno di due righe:
grant usage on schema billing to authenticated;
grant select on billing.invoices to authenticated;
Se le tue tabelle rispondono a uno sconosciuto è una cosa che si controlla da fuori. La nostra scansione gratuita legge la tua app online con la stessa chiave pubblica che riceve un visitatore, ti dice quali tabelle restituiscono righe e non richiede un account.
Esporre lo schema o spostare la tabella?
Decidi in base a chi legge la tabella. Una tabella che la tua app legge dal
browser ha bisogno di un permesso per il ruolo che la legge e della Row Level
Security con una regola, ovunque si trovi. Spostarla in public ti risparmia il
passaggio dell'esposizione, e nient'altro.
Se la sposti:
alter table billing.invoices set schema public;
La tabella tiene i suoi permessi e le sue regole. Una funzione il cui corpo nomina
billing.invoices fallisce poi con questo stesso errore, perché il corpo tiene il
nome così come è stato scritto.
Una tabella che serve solo al codice server può restare a un piano dove
l'ascensore non va mai. Una funzione in public può fare con lei l'unico lavoro di
cui la tua app ha bisogno, e
chiamare una funzione Supabase spiega come
togliere execute ai visitatori che non hanno fatto l'accesso.
Causa 3: una funzione con il percorso di ricerca vuoto
Una funzione impostata su search_path = '' non percorre nessun piano, quindi
ogni tabella che usa ha bisogno del suo schema davanti: public.invoices, mai un
invoices da solo.
Il Security Advisor di Supabase chiede proprio questa impostazione. Il suo
avviso function_search_path_mutable
dice: "We recommend pinning functions' search_path to an empty string". La stessa
pagina finisce così: "once you set the search_path = '' all references to
tables/functions/views/etc in your function's body must be qualified with a
schema name."
Quindi l'avviso si risolve con una riga, e la funzione fallisce poi sul primo nome di tabella senza schema:
create or replace function public.count_invoices()
returns bigint
language plpgsql
set search_path = ''
as $$
begin
return (select count(*) from invoices); -- relation "invoices" does not exist
end;
$$;
Scrivi from public.invoices e torna a funzionare. Tieni l'impostazione: serve a
impedire che chi chiama la funzione faccia puntare un nome senza schema a una
tabella sua.
Chiamata dalla tua app con .rpc(), l'errore attraversa PostgREST come 42P01
con HTTP 404, lo stato che
il codice di PostgREST
dà a una tabella sconosciuta. Un trigger sulle nuove registrazioni che fallisce
così compare come
"Database error saving new user".
Causa 4: la tabella non è ancora in quel progetto
La tabella esiste in un altro database, o solo nel tuo codice.
Un account Supabase può contenere più progetti, e un branch è un progetto a sé con il suo database. Una tabella creata nel SQL Editor del progetto sbagliato è una stanza in un altro edificio. La versione per colonne di questo errore mostra come confrontare il riferimento del progetto nell'indirizzo Supabase della tua app con quello nella dashboard.
Se costruisci con Lovable, ripercorri la chat cercando SQL che ti è stato chiesto di approvare. La documentazione Supabase di Lovable dice che "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Finché non approvi, la tabella esiste solo nel codice che la legge.
Spostare una tabella da cui dipende la tua app
Tieni una copia del database di prima dello spostamento, e vieni a saperlo quando cambia qualcosa di ciò che la tua app espone.
Reeve Care conserva una copia del tuo database Supabase:
- una copia cifrata secondo un calendario, sullo spazio di archiviazione di Reeve nell'UE, fuori dal tuo account Supabase
- ogni schema che hai creato,
publice gli altri, con le loro tabelle e righe com'erano prima della tua modifica - ogni copia verificata prima di contare, contando le righe di ogni tabella
- anche i tuoi file caricati, appena colleghi una credenziale di Storage
- un ripristino con un clic, con una copia di sicurezza dello stato attuale presa prima
- tutto quello che fa Monitor
Reeve Monitor continua a controllare da fuori:
- se la tua app è raggiungibile, controllato ogni minuto
- il controllo di sicurezza completo ogni ora, su un massimo di tre app
- un messaggio quando un risultato cambia, così un deploy che cambia ciò che la tua app espone ti raggiunge senza che tu debba andare a guardare
- un report mensile, in linguaggio semplice, di quello che ha visto
Che cosa fare adesso
Cosa fare
- Leggi il messaggio: il nome tra virgolette è quello che è stato chiesto, e
public.davanti è lo schema che è stato percorso. - Confronta il nome con il Table Editor, maiuscole comprese. Una tabella con una maiuscola nel nome vuole le virgolette doppie in SQL.
- Trova in quale schema si trova la tabella con la query su
information_schema.tables, e chiama una tabella fuori dapubliccon.schema('nome').from(...). - Prima di esporre uno schema, esegui la query su
pg_tablese attiva la Row Level Security su ogni tabella che dicefalse. - Dentro una funzione con
search_path = '', scrivi ogni tabella comepublic.nome. - Controlla che il progetto che la tua app chiama sia quello in cui hai creato la tabella.
Comincia dai due nomi nel messaggio, la tabella e lo schema davanti, perché dicono esattamente quale porta è stata chiesta e a quale piano. Prima di spostare, rinominare o esporre qualcosa che le persone usano, tieni una copia del database di prima della modifica.
FAQ
Perché Supabase dice che la mia tabella non esiste?
Perché nello schema percorso dalla query non c'è nessuna tabella con esattamente quel nome. Controlla quattro cose in ordine: le maiuscole del nome, lo schema in cui si trova la tabella (la tua app cerca in public se non le dici altro), il progetto con cui parla la tua app, e se la query gira dentro una funzione con il search_path vuoto, dove ogni tabella ha bisogno del suo schema davanti.
Che cosa significa "the schema must be one of the following"?
È l'errore PGRST106 nella formulazione delle versioni più vecchie di PostgREST. La tua app ha chiesto uno schema che il progetto non espone, per esempio con .schema('nome'), e l'elenco che segue sono gli schemi esposti. Le versioni più recenti dicono "Invalid schema: nome" e mettono lo stesso elenco nel suggerimento. Esporre lo schema lo risolve, ed è una decisione da prendere con cognizione.
È sicuro esporre uno schema in Supabase?
Solo quando ogni tabella che contiene ha la Row Level Security attivata e una regola scritta. La guida di Supabase sugli schemi personalizzati dà ad anon, il ruolo di ogni visitatore che non ha fatto l'accesso, accesso completo a ogni tabella dello schema, comprese quelle create dopo. La sua guida alla sicurezza aggiunge che le tabelle create nel SQL Editor non hanno la Row Level Security per impostazione predefinita.
Le maiuscole contano nel nome di una tabella Supabase?
Sì. Un nome scritto in SQL senza virgolette doppie viene portato in minuscolo, mentre il Table Editor e supabase-js tengono le maiuscole esattamente come sono state scritte. Una tabella creata come Invoices si scrive "Invoices", tra virgolette, in SQL, e .from('Invoices') nella tua app.
Conviene spostare la tabella in public?
Spostarla risolve la metà dell'errore che riguarda lo schema e niente della sicurezza: una tabella che la tua app legge dal browser ha bisogno di un permesso per il ruolo che la legge e della Row Level Security con una regola, ovunque si trovi. Una tabella che serve solo al codice server può restare in uno schema non esposto, raggiunta da una funzione in public che fa l'unico lavoro di cui la tua app ha bisogno.