Vai al contenuto

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.

Vlad Tkachenko12 min di lettura
Un edificio in sezione accanto al vano di un ascensore. L'ascensore è al piano terra, e la sua fermata un piano più su è un vuoto tratteggiato color ambra.

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 queryChe cosa torna
Il SQL Editor, una migrazione, una funzione o un triggerrelation "invoices" does not exist, codice 42P01
La tua app, con PostgREST 13 o successivoCould not find the table 'public.invoices' in the schema cache, codice PGRST205, HTTP 404
La tua app, con un PostgREST più vecchiouna 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 esponeInvalid 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 Invoices resta Invoices
  • PostgREST mette tra virgolette doppie ogni nome che la tua app invia (pgFmtIdent), quindi .from('invoices') chiede invoices e nient'altro
Come è stata creata la tabellaIn SQL, scriviNella tua app, scrivi
create table invoices, o Invoices scritto in SQL senza virgoletteinvoices.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 billing o private, 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 in public
  • 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 in public, profiles nel 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.

L'ascensore si ferma solo ai piani esposti. Una tabella a un altro piano manca da public, e chiedere quel piano per nome viene rifiutato finché non è esposto.

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 un piano con i permessi della guida apre ogni porta che c'è. La Row Level Security è il lucchetto di ogni stanza, e una stanza senza lucchetto è aperta a chiunque abbia la tua chiave pubblica.

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

Con il percorso di ricerca vuoto non c'è nessun piano da percorrere, quindi solo un indirizzo completo trova la stanza.

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, public e 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 da public con .schema('nome').from(...).
  • Prima di esporre uno schema, esegui la query su pg_tables e attiva la Row Level Security su ogni tabella che dice false.
  • Dentro una funzione con search_path = '', scrivi ogni tabella come public.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.

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.