Aller au contenu

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

Vlad Tkachenko12 min de lecture
Un immeuble en coupe à côté d'une cage d'ascenseur. L'ascenseur est au rez-de-chaussée, et son arrêt un étage plus haut est un vide en pointillés ambre.

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éeCe qui revient
Le SQL Editor, une migration, une fonction ou un triggerrelation "invoices" does not exist, code 42P01
Votre application, avec PostgREST 13 ou plus récentCould not find the table 'public.invoices' in the schema cache, code PGRST205, HTTP 404
Votre application, avec un PostgREST plus ancienune 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 pasInvalid 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 Invoices reste Invoices
  • PostgREST met entre guillemets doubles chaque nom que votre application envoie (pgFmtIdent), donc .from('invoices') demande invoices et rien d'autre
Comment la table a été crééeEn SQL, écrivezDans votre application, écrivez
create table invoices, ou Invoices tapé en SQL sans guillemetsinvoices.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 billing ou private, 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 dans public
  • 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 dans public, profiles dans 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.

L'ascenseur ne s'arrête qu'aux étages exposés. Une table à un autre étage est absente de public, et demander cet étage par son nom est refusé tant qu'il n'est pas exposé.

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 un étage avec les droits du guide ouvre chaque porte qui s'y trouve. La Row Level Security est le cadenas de chaque pièce, et une pièce sans cadenas est ouverte à quiconque a votre clé publique.

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

Avec un chemin de recherche vide, il n'y a aucun étage à parcourir, donc seule une adresse complète trouve la pièce.

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éé, public et 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 de public avec .schema('nom').from(...).
  • Avant d'exposer un schéma, exécutez la requête sur pg_tables et activez la Row Level Security sur chaque table qui affiche false.
  • Dans une fonction avec search_path = '', écrivez chaque table sous la forme public.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.

Écrit par

Vlad Tkachenko

Fondateur de Reeve

Je passe mes journées à examiner des applications créées avec Lovable, Bolt, v0, Cursor et Replit, et la courte liste d'erreurs qui y reviennent sans cesse.

En savoir plus sur l'auteur

À lire ensuite

Tous les articles

Sauvegardez votre base Supabase avant la prochaine modification

Reeve en garde une copie chiffrée à intervalles réguliers et la restaure pour vous en cas de problème.

Voir les sauvegardes Supabase

Vérification externe automatisée, pas un audit complet. L'absence de problème n'est pas une garantie de sécurité.