Bases de la sécurité
Supabase "relation does not exist" alors que la table existe
Supabase "relation does not exist" ou PGRST205 : aucune table ne porte ce nom dans ce schéma. Quatre vérifications, et ce qu'ouvre un schéma exposé.

En bref
- Supabase "relation does not exist" (code Postgres 42P01) signifie qu'aucune table ni vue portant exactement ce nom ne se trouve dans les schémas que la requête a parcourus. Depuis votre application, sur un projet récent, le même problème se lit "Could not find the table in the schema cache" (PGRST205).
- Vérifiez d'abord le nom lettre par lettre, majuscules comprises, puis le schéma, puis le projet. Dans une fonction dont le search_path est vide, chaque table doit porter son schéma devant : public.invoices.
- Exposer un schéma fait disparaître l'erreur et rend chaque table qu'il contient accessible par l'API. Vérifiez donc la Row Level Security de chacune de ces tables avant de le faire.
Votre builder a ajouté une table invoices. Vous la voyez dans le Table Editor,
avec ses lignes, et la page qui devrait les lister reste vide. La console du
navigateur affiche ceci :
{
"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" signifie que Postgres n'a trouvé aucune table
ni vue portant exactement ce nom dans les schémas que la requête a parcourus.
PGRST205 est la même réponse venant de PostgREST, la partie de Supabase qui
transforme les appels supabase.from(...) de votre application en requêtes de
base de données, donnée avant même que la requête n'atteigne Postgres.
La solution qui circule pour cette erreur consiste à ajouter le schéma à Exposed schemas dans les réglages du projet. Elle règle l'une des quatre causes ci-dessous. Elle rend aussi chaque table de ce schéma accessible à quiconque détient la clé publique de votre application. Lisez donc ce qu'elle ouvre avant de l'activer.
Que signifie "relation does not exist" dans Supabase ?
La base de données a cherché une table portant exactement ce nom, dans les schémas
qu'on lui a demandé de parcourir, et n'en a trouvé aucune. "Relation" est le mot
de Postgres pour tout ce d'où sortent des lignes, une table ou une vue. Le code de
l'erreur est 42P01, que
Postgres appelle undefined_table.
Imaginez votre base de données comme un immeuble. Chaque schéma est un étage :
public est le rez-de-chaussée, et auth, où Supabase garde vos utilisateurs, en
est un autre. Chaque table est une pièce avec son nom sur la porte. Une requête qui
donne seulement le nom d'une pièce la cherche aux étages listés dans son chemin de
recherche. Les requêtes de votre application prennent un ascenseur, et l'ascenseur
ne s'arrête qu'aux étages que votre projet expose. L'erreur signifie que personne
n'a trouvé de porte à ce nom aux étages visités.
La formulation que vous voyez dépend de l'endroit où la requête s'est exécutée :
| Où la requête s'est exécutée | Ce qui revient |
|---|---|
| Le SQL Editor, une migration, une fonction ou un trigger | relation "invoices" does not exist, code 42P01 |
| Votre application, avec PostgREST 13 ou plus récent | Could not find the table 'public.invoices' in the schema cache, code PGRST205, HTTP 404 |
| Votre application, avec un PostgREST plus ancien | une lecture reçoit relation "public.invoices" does not exist, HTTP 404, et un insert une erreur vide |
| Votre application, qui demande un schéma que le projet n'expose pas | Invalid schema: billing, code PGRST106, HTTP 406 |
Le changelog de PostgREST
date la nouvelle formulation de la version 13.0.0, publiée le 8 mai 2025 :
"Querying non-existent table now returns PGRST205 error". Avant cela, comme l'a
expliqué l'un de ses mainteneurs dans l'issue #3697,
une lecture partait vers Postgres sans vérification et revenait en 42P01. Le
guide Supabase sur la sécurisation de la Data API
indique que vous pouvez vérifier la version de PostgREST de votre projet dans le
Dashboard. Le code de PostgREST
fixe le reste : l'indice de PGRST205, quand il y en a un, nomme la table la plus
proche qu'il connaît au même étage.
Si le message dit plutôt permission denied for table, la table a été trouvée et
votre application a été refoulée à la porte. Celle-là demande
une autre solution.
Cause 1 : le nom sur la porte a une majuscule
Le nom doit correspondre exactement, majuscules comprises, et deux des outils que vous utilisez traitent les majuscules autrement que le troisième.
La documentation de Postgres résume la règle en une phrase : "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case." Les deux autres gardent ce que vous avez tapé :
- le Table Editor de Supabase crée une table sous exactement le nom que vous lui
donnez.
Son code
met le nom entre guillemets quand il en a besoin, donc
InvoicesresteInvoices - PostgREST met entre guillemets doubles chaque nom que votre application envoie
(
pgFmtIdent), donc.from('invoices')demandeinvoiceset rien d'autre
| Comment la table a été créée | En SQL, écrivez | Dans votre application, écrivez |
|---|---|---|
create table invoices, ou Invoices tapé en SQL sans guillemets | invoices | .from('invoices') |
create table "Invoices", ou Invoices tapé dans le Table Editor | "Invoices", guillemets compris | .from('Invoices') |
La deuxième ligne produit cette erreur des deux côtés. Votre application appelle
.from('invoices') et reçoit PGRST205, avec le vrai nom dans l'indice, comme dans
le message en haut de cette page. Ou l'application fonctionne, et une fonction ou
une policy qui nomme Invoices sans guillemets échoue avec
relation "invoices" does not exist, parce que Postgres a mis le nom en
minuscules avant de chercher.
Pour voir chaque table portant ce nom, à n'importe quel étage et avec n'importe quelles majuscules, exécutez ceci dans le SQL Editor :
select table_schema, table_name
from information_schema.tables
where table_name ilike 'invoices';
La solution qui ne touche à rien dans vos données se trouve dans le code : écrivez
"Invoices" avec ses guillemets en SQL, et les majuscules exactes dans
l'application. Renommer la table à la place casse chaque écran qui utilise encore
l'ancien nom.
La version colonne de cette erreur a
le même piège pour les noms de colonnes.
Cause 2 : la table est à un autre étage
Votre application cherche dans public sauf indication contraire, donc une table
de n'importe quel autre schéma est absente pour elle. C'est pourquoi le message dit
public.invoices : public est l'étage où l'ascenseur est allé.
Cela arrive de trois façons :
- votre builder a créé la table dans un schéma à lui, comme
billingouprivate, et l'application appelle un simple.from('invoices') - le schéma est écrit dans le nom, comme
.from('billing.invoices'). PostgREST lit toute la chaîne comme un seul nom de table et la cherche danspublic - la table est
auth.users. Le guide Supabase sur les données utilisateur dit : "For security, the Auth schema is not exposed in the auto-generated API." Sa réponse est une table à vous danspublic,profilesdans son exemple, remplie par un trigger quand quelqu'un s'inscrit, et ce trigger peut échouer à sa manière
Une table à un autre étage se lit avec .schema('billing').from('invoices'), et ce
schéma doit figurer dans Exposed schemas dans les réglages d'API du projet, comme
le montre le
guide Supabase sur les schémas personnalisés.
Tant que ce n'est pas le cas, la réponse devient PGRST106 :
{
"code": "PGRST106",
"details": null,
"hint": "Only the following schemas are exposed: public, graphql_public",
"message": "Invalid schema: billing"
}
L'indice liste les étages où l'ascenseur s'arrête bien. PostgREST 12 formule la
même erreur autrement, avec la liste dans le message lui-même :
The schema must be one of the following: public, graphql_public. C'est le texte
que produit son code pour cette version,
et le changelog a placé le nom du schéma dans le message en 13.0.7.
Votre clé secrète prend le même ascenseur. L'exposition est un réglage de l'API,
donc du code serveur qui utilise supabase-js avec cette clé reçoit le même
PGRST106.
Qu'ouvre un schéma exposé ?
L'ascenseur s'arrête alors à cet étage, et avec les droits que donne le guide de Supabase, chaque visiteur reçoit une clé de chaque pièce. La Row Level Security est alors la seule chose qui décide de ce que chacun peut lire ou modifier.
Le guide sur les schémas personnalisés compte deux étapes. La première est le réglage Exposed schemas. La seconde est un bloc de SQL, dont voici les deux premières lignes :
GRANT USAGE ON SCHEMA myschema TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA myschema TO anon, authenticated, service_role;
anon est le rôle derrière chaque requête de quelqu'un qui n'est pas connecté.
ALL veut dire lire, ajouter, modifier et supprimer. Le bloc continue avec
ALTER DEFAULT PRIVILEGES, qui donne le même accès à chaque table créée plus tard
à cet étage.
C'est sûr là où la Row Level Security, les règles par ligne que Supabase vérifie à chaque requête, est activée et écrite pour chaque table. Le guide Supabase sur la sécurisation de la Data API dit : "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." Un schéma que votre builder a mis en place en SQL peut contenir des tables où elle est désactivée. Exposée avec ces droits, une telle table livre chaque ligne à quiconque a votre clé publique, et accepte aussi ses modifications.
Vérifiez avant d'exposer :
select tablename, rowsecurity
from pg_tables
where schemaname = 'billing';
Chaque ligne qui dit false est une table sans aucune règle.
Activer la Row Level Security sur chaque table
couvre l'activation et la première règle, et
"RLS disabled in public" couvre la règle
de lecture ouverte qui laisse une table tout aussi accessible une fois la Row
Level Security activée.
Vous pouvez aussi donner moins de droits que le guide. Un étage avec une table que seuls les utilisateurs connectés lisent a besoin de deux lignes :
grant usage on schema billing to authenticated;
grant select on billing.invoices to authenticated;
Savoir si vos tables répondent à un inconnu, cela se vérifie de l'extérieur. Notre scan gratuit lit votre application en ligne avec la même clé publique qu'un visiteur reçoit, indique quelles tables rendent des lignes, et ne demande aucun compte.
Exposer le schéma ou déplacer la table ?
Décidez selon qui lit la table. Une table que votre application lit depuis le
navigateur a besoin d'un droit pour le rôle qui la lit et de la Row Level Security
avec une règle, où qu'elle se trouve. La déplacer dans public vous épargne donc
l'étape de l'exposition, et rien de plus.
Si vous la déplacez :
alter table billing.invoices set schema public;
La table garde ses droits et ses règles. Une fonction dont le corps nomme
billing.invoices échoue ensuite avec cette même erreur, parce que le corps garde
le nom tel qu'il a été écrit.
Une table dont seul le code serveur a besoin peut rester à un étage où l'ascenseur
ne va jamais. Une fonction dans public peut faire avec elle la seule tâche dont
votre application a besoin, et
appeler une fonction Supabase montre comment
retirer execute aux visiteurs non connectés.
Cause 3 : une fonction avec un chemin de recherche vide
Une fonction réglée sur search_path = '' ne parcourt aucun étage, donc chaque
table qu'elle utilise doit porter son schéma devant : public.invoices, jamais un
invoices seul.
Le Security Advisor de Supabase demande justement ce réglage. Son
avertissement function_search_path_mutable
dit : "We recommend pinning functions' search_path to an empty string". La même
page se termine par : "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."
L'avertissement se corrige donc avec une ligne, et la fonction échoue ensuite sur le premier nom de table sans schéma :
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;
$$;
Écrivez from public.invoices et elle fonctionne de nouveau. Gardez le réglage :
il empêche quiconque appelle la fonction de faire pointer un nom sans schéma vers
une table à lui.
Appelée depuis votre application avec .rpc(), l'erreur traverse PostgREST en
42P01 avec HTTP 404, le statut que
le code de PostgREST
donne à une table inconnue. Un trigger sur les nouvelles inscriptions qui échoue
ainsi apparaît comme
"Database error saving new user".
Cause 4 : la table n'est pas encore dans ce projet
La table existe dans une autre base de données, ou seulement dans votre code.
Un compte Supabase peut contenir plusieurs projets, et une branche est un projet à part avec sa propre base de données. Une table créée dans le SQL Editor du mauvais projet est une pièce dans un autre immeuble. La version colonne de cette erreur montre comment comparer la référence du projet dans l'adresse Supabase de votre application avec celle du dashboard.
Si vous construisez avec Lovable, remontez le chat à la recherche d'un SQL qu'on vous a demandé d'approuver. La documentation Supabase de Lovable dit qu'il "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Tant que vous n'avez pas approuvé, la table n'existe que dans le code qui la lit.
Déplacer une table dont votre application dépend
Gardez une copie de la base de données d'avant le déplacement, et soyez prévenu quand quelque chose change dans ce que votre application expose.
Reeve Care garde une copie de votre base de données Supabase :
- une copie chiffrée selon un calendrier, sur le stockage propre de Reeve dans l'UE, en dehors de votre compte Supabase
- chaque schéma que vous avez créé,
publicet les autres, avec leurs tables et leurs lignes telles qu'elles étaient avant votre changement - chaque copie vérifiée avant de compter, en comptant les lignes de chaque table
- vos fichiers envoyés aussi, dès que vous connectez un identifiant Storage
- une restauration en un clic, avec une copie de sécurité de l'état actuel prise d'abord
- tout ce que fait Monitor
Reeve Monitor continue de vérifier de l'extérieur :
- si votre application répond, vérifié chaque minute
- la vérification de sécurité complète toutes les heures, sur trois applications au plus
- un message quand un résultat change, pour qu'un déploiement qui modifie ce que votre application expose vous parvienne sans que vous alliez voir
- un rapport mensuel, en langage clair, de ce qu'il a vu
Que faire maintenant
Que faire
- Lisez le message : le nom entre guillemets est ce qui a été demandé, et
public.devant est le schéma qui a été parcouru. - Comparez le nom avec le Table Editor, majuscules comprises. Une table avec une majuscule dans son nom demande des guillemets doubles en SQL.
- Trouvez dans quel schéma se trouve la table avec la requête sur
information_schema.tables, et appelez une table hors depublicavec.schema('nom').from(...). - Avant d'exposer un schéma, exécutez la requête sur
pg_tableset activez la Row Level Security sur chaque table qui affichefalse. - Dans une fonction avec
search_path = '', écrivez chaque table sous la formepublic.nom. - Vérifiez que le projet que votre application appelle est celui où vous avez créé la table.
Commencez par les deux noms du message, la table et le schéma devant elle, car ils disent exactement quelle porte a été demandée et à quel étage. Avant de déplacer, renommer ou exposer quoi que ce soit que des gens utilisent, gardez une copie de la base de données d'avant le changement.
FAQ
Pourquoi Supabase dit-il que ma table n'existe pas ?
Parce qu'aucune table portant exactement ce nom ne se trouve dans le schéma que la requête a parcouru. Vérifiez quatre choses dans l'ordre : les majuscules du nom, le schéma où se trouve la table (votre application cherche dans public sauf indication contraire), le projet auquel votre application parle, et si la requête s'exécute dans une fonction dont le search_path est vide, où chaque table doit porter son schéma devant.
Que signifie "the schema must be one of the following" ?
C'est l'erreur PGRST106 dans la formulation des anciennes versions de PostgREST. Votre application a demandé un schéma que le projet n'expose pas, par exemple avec .schema('nom'), et la liste qui suit est celle des schémas exposés. Les versions récentes disent "Invalid schema: nom" et placent la même liste dans l'indice. Exposer le schéma règle le problème, et cela mérite une décision réfléchie.
Est-il sûr d'exposer un schéma dans Supabase ?
Seulement une fois que chaque table qu'il contient a la Row Level Security activée et une règle écrite. Le guide Supabase sur les schémas personnalisés donne à anon, le rôle de tout visiteur non connecté, un accès complet à chaque table du schéma, y compris aux tables créées plus tard. Son guide de sécurité ajoute que les tables créées dans le SQL Editor n'ont pas la Row Level Security par défaut.
Les majuscules comptent-elles dans un nom de table Supabase ?
Oui. Un nom écrit en SQL sans guillemets doubles est mis en minuscules, alors que le Table Editor et supabase-js gardent les majuscules telles qu'elles ont été écrites. Une table créée sous le nom Invoices s'écrit "Invoices", entre guillemets, en SQL, et .from('Invoices') dans votre application.
Faut-il plutôt déplacer la table dans public ?
La déplacer règle la moitié schéma de l'erreur et rien côté sécurité : une table que votre application lit depuis le navigateur a besoin d'un droit pour le rôle qui la lit et de la Row Level Security avec une règle, où qu'elle se trouve. Une table dont seul le code serveur a besoin peut rester dans un schéma non exposé, atteinte par une fonction dans public qui fait la seule tâche dont votre application a besoin.