Zum Inhalt springen

Sicherheitsgrundlagen

Supabase "relation does not exist", obwohl die Tabelle da ist

Supabase "relation does not exist" oder PGRST205: Keine Tabelle passt zu Name und Schema deiner Abfrage. Vier Prüfungen, und was eine Freigabe öffnet.

Vlad Tkachenko11 Min. Lesezeit
Ein Gebäude im Querschnitt neben einem Aufzugsschacht. Der Aufzug steht im Erdgeschoss, sein Halt eine Etage höher ist eine gestrichelte bernsteinfarbene Lücke.

Kurz gesagt

  • Supabase "relation does not exist" (Postgres-Code 42P01) heißt: In den Schemas, die die Abfrage durchsucht hat, gibt es keine Tabelle und keinen View mit genau diesem Namen. Aus deiner App heißt dasselbe Problem bei einem aktuellen Projekt "Could not find the table in the schema cache" (PGRST205).
  • Prüf zuerst den Namen Buchstabe für Buchstabe, samt Großbuchstaben, dann das Schema, dann das Projekt. In einer Funktion mit leerem search_path braucht jede Tabelle ihr Schema davor: public.invoices.
  • Ein Schema freizugeben lässt den Fehler verschwinden und macht jede Tabelle darin über die API erreichbar. Prüf also Row Level Security auf jeder dieser Tabellen, bevor du es tust.

Dein Builder hat eine Tabelle invoices angelegt. Du siehst sie im Table Editor, samt Zeilen, und die Seite, die sie auflisten soll, bleibt leer. Die Browserkonsole zeigt das hier:

{
  "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" heißt, dass Postgres in den Schemas, die die Abfrage durchsucht hat, keine Tabelle und keinen View mit genau diesem Namen gefunden hat. PGRST205 ist dieselbe Antwort von PostgREST, dem Teil von Supabase, der die Aufrufe supabase.from(...) deiner App in Datenbankabfragen übersetzt, gegeben, bevor die Abfrage Postgres überhaupt erreicht.

Die Lösung, die für diesen Fehler herumgereicht wird, lautet: das Schema in den Projekteinstellungen unter Exposed schemas eintragen. Das behebt eine der vier Ursachen unten. Es macht außerdem jede Tabelle in diesem Schema für jeden erreichbar, der den öffentlichen Schlüssel deiner App hat. Lies also, was es öffnet, bevor du es einschaltest.

Was bedeutet "relation does not exist" in Supabase?

Die Datenbank hat eine Tabelle mit genau diesem Namen gesucht, in den Schemas, die sie durchsuchen sollte, und keine gefunden. "Relation" ist das Wort von Postgres für alles, woraus Zeilen kommen, also eine Tabelle oder einen View. Der Code des Fehlers ist 42P01, den Postgres undefined_table nennt.

Stell dir deine Datenbank als Gebäude vor. Jedes Schema ist eine Etage: public ist das Erdgeschoss, und auth, wo Supabase deine Nutzer aufbewahrt, ist eine andere. Jede Tabelle ist ein Raum mit ihrem Namen an der Tür. Eine Abfrage, die nur den Namen eines Raums nennt, wird auf den Etagen gesucht, die in ihrem Suchpfad stehen. Die Anfragen deiner App fahren mit einem Aufzug, und der Aufzug hält nur auf den Etagen, die dein Projekt freigibt. Der Fehler heißt: Auf den Etagen, die geprüft wurden, hat niemand eine Tür mit diesem Namen gefunden.

Welchen Wortlaut du siehst, hängt davon ab, wo die Abfrage lief:

Wo die Abfrage liefWas zurückkommt
Im SQL Editor, in einer Migration, einer Funktion oder einem Triggerrelation "invoices" does not exist, Code 42P01
In deiner App, mit PostgREST 13 oder neuerCould not find the table 'public.invoices' in the schema cache, Code PGRST205, HTTP 404
In deiner App, mit einem älteren PostgRESTein Lesezugriff bekommt relation "public.invoices" does not exist, HTTP 404, und ein Insert einen leeren Fehler
In deiner App, die ein Schema anfragt, das das Projekt nicht freigibtInvalid schema: billing, Code PGRST106, HTTP 406

Das Changelog von PostgREST datiert den neueren Wortlaut auf Version 13.0.0, erschienen am 8. Mai 2025: "Querying non-existent table now returns PGRST205 error". Davor ging ein Lesezugriff, wie einer der Maintainer in Issue #3697 erklärt, ungeprüft an Postgres und kam als 42P01 zurück. Supabases Anleitung zur Absicherung der Data API sagt, dass du die PostgREST-Version deines Projekts im Dashboard prüfen kannst. Der eigene Code von PostgREST legt den Rest fest: Der Hinweis von PGRST205 nennt, wenn es einen gibt, die ähnlichste Tabelle, die es auf derselben Etage kennt.

Steht in der Meldung stattdessen permission denied for table, wurde die Tabelle gefunden, und deine App wurde an der Tür abgewiesen. Dafür gibt es eine andere Lösung.

Ursache 1: Der Name an der Tür hat einen Großbuchstaben

Der Name muss genau passen, samt Großbuchstaben, und zwei der Werkzeuge, die du benutzt, behandeln Großbuchstaben anders als das dritte.

Die Dokumentation von Postgres fasst die Regel in einem Satz: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case." Die beiden anderen behalten, was du getippt hast:

  • Der Table Editor von Supabase legt eine Tabelle unter genau dem Namen an, den du ihr gibst. Sein Code setzt den Namen in Anführungszeichen, wenn er sie braucht, also bleibt InvoicesInvoices.
  • PostgREST setzt jeden Namen, den deine App schickt, in doppelte Anführungszeichen (pgFmtIdent), also fragt .from('invoices') nach invoices und nach nichts anderem.
Wie die Tabelle angelegt wurdeIn SQL schreibst duIn deiner App schreibst du
create table invoices, oder Invoices in SQL ohne Anführungszeicheninvoices.from('invoices')
create table "Invoices", oder Invoices im Table Editor eingetippt"Invoices", mit Anführungszeichen.from('Invoices')

Die zweite Zeile erzeugt diesen Fehler von beiden Seiten. Deine App ruft .from('invoices') auf und bekommt PGRST205, mit dem echten Namen im Hinweis, wie in der Meldung oben auf dieser Seite. Oder die App funktioniert, und eine Funktion oder eine Policy, die Invoices ohne Anführungszeichen nennt, scheitert mit relation "invoices" does not exist, weil Postgres den Namen umgewandelt hat, bevor es suchte.

Um jede Tabelle mit diesem Namen zu sehen, auf jeder Etage und in jeder Schreibweise, führ das im SQL Editor aus:

select table_schema, table_name
from information_schema.tables
where table_name ilike 'invoices';

Die Lösung, die an deinen Daten nichts ändert, liegt im Code: Schreib in SQL "Invoices" mit seinen Anführungszeichen und in der App genau die Großbuchstaben. Die Tabelle stattdessen umzubenennen bricht jede Seite, die noch den alten Namen benutzt. Die Spaltenversion dieses Fehlers hat dieselbe Falle bei Spaltennamen.

Ursache 2: Die Tabelle liegt auf einer anderen Etage

Deine App sucht in public, solange man ihr nichts anderes sagt, also fehlt eine Tabelle in jedem anderen Schema, soweit sie das sehen kann. Deshalb sagt die Meldung public.invoices: public ist die Etage, zu der der Aufzug gefahren ist.

Das passiert auf drei Arten:

  • dein Builder hat die Tabelle in einem eigenen Schema angelegt, etwa billing oder private, und die App ruft ein schlichtes .from('invoices') auf
  • das Schema steht im Namen, als .from('billing.invoices'). PostgREST liest die ganze Zeichenkette als einen Tabellennamen und sucht ihn in public
  • die Tabelle ist auth.users. Supabases Anleitung zu Nutzerdaten sagt: "For security, the Auth schema is not exposed in the auto-generated API." Ihre Antwort ist eine eigene Tabelle in public, in ihrem Beispiel profiles, gefüllt von einem Trigger, wenn sich jemand registriert, und dieser Trigger kann auf eigene Arten scheitern

Eine Tabelle auf einer anderen Etage liest du mit .schema('billing').from('invoices'), und dieses Schema muss in den API-Einstellungen des Projekts unter Exposed schemas stehen, wie Supabases Anleitung zu eigenen Schemas zeigt. Bis dahin ändert sich die Antwort zu PGRST106:

{
  "code": "PGRST106",
  "details": null,
  "hint": "Only the following schemas are exposed: public, graphql_public",
  "message": "Invalid schema: billing"
}

Der Hinweis zählt die Etagen auf, an denen der Aufzug hält. PostgREST 12 formuliert denselben Fehler anders, mit der Liste in der Meldung selbst: The schema must be one of the following: public, graphql_public. Das ist der Text, den sein Code für diese Version erzeugt, und laut Changelog kam der Schemaname in 13.0.7 in die Meldung.

Dein geheimer Schlüssel fährt im selben Aufzug. Die Freigabe ist eine Einstellung der API, also bekommt Servercode, der supabase-js mit diesem Schlüssel benutzt, dasselbe PGRST106.

Der Aufzug hält nur auf freigegebenen Etagen. Eine Tabelle auf einer anderen Etage fehlt in public, und wer diese Etage beim Namen verlangt, wird abgewiesen, bis sie freigegeben ist.

Was öffnet ein freigegebenes Schema?

Der Aufzug hält dann auf dieser Etage, und mit den Berechtigungen aus Supabases Anleitung bekommt jeder Besucher einen Schlüssel zu jedem Raum dort. Row Level Security entscheidet dann als Einziges, was jeder von ihnen lesen oder ändern kann.

Die Anleitung zu eigenen Schemas hat zwei Schritte. Der erste ist die Einstellung Exposed schemas. Der zweite ist ein SQL-Block, und seine ersten beiden Zeilen lauten:

GRANT USAGE ON SCHEMA myschema TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA myschema TO anon, authenticated, service_role;

anon ist die Rolle hinter jeder Anfrage von jemandem, der nicht angemeldet ist. ALL heißt lesen, hinzufügen, ändern und löschen. Der Block geht weiter mit ALTER DEFAULT PRIVILEGES, das denselben Zugriff jeder Tabelle gibt, die später auf dieser Etage angelegt wird.

Sicher ist das dort, wo Row Level Security, die Regeln pro Zeile, die Supabase bei jeder Anfrage prüft, eingeschaltet und für jede Tabelle geschrieben ist. Supabases Anleitung zur Absicherung der Data API sagt: "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." Ein Schema, das dein Builder in SQL eingerichtet hat, kann Tabellen ohne sie enthalten. Mit diesen Berechtigungen freigegeben, gibt so eine Tabelle jede Zeile an jeden heraus, der deinen öffentlichen Schlüssel hat, und nimmt auch seine Änderungen an.

Prüf vor der Freigabe:

select tablename, rowsecurity
from pg_tables
where schemaname = 'billing';

Jede Zeile mit false ist eine Tabelle ganz ohne Regeln. Row Level Security für jede Tabelle einschalten behandelt das Einschalten und die erste Regel, und "RLS disabled in public" behandelt die offene Leseregel, die eine Tabelle auch mit eingeschalteter Row Level Security genauso erreichbar lässt.

Du kannst auch weniger freigeben als die Anleitung. Eine Etage mit einer Tabelle, die nur angemeldete Nutzer lesen, braucht zwei Zeilen:

grant usage on schema billing to authenticated;
grant select on billing.invoices to authenticated;

Ob deine Tabellen einem Fremden antworten, kannst du von außen prüfen. Unser kostenloser Scan liest deine laufende App mit demselben öffentlichen Schlüssel, den ein Besucher bekommt, meldet, welche Tabellen Zeilen herausgeben, und braucht kein Konto.

Eine Etage mit den Berechtigungen der Anleitung freizugeben öffnet jede Tür darauf. Row Level Security ist das Vorhängeschloss in jedem Raum, und ein Raum ohne eines steht jedem offen, der deinen öffentlichen Schlüssel hat.

Schema freigeben oder Tabelle verschieben?

Entscheide danach, wer die Tabelle liest. Eine Tabelle, die deine App aus dem Browser liest, braucht eine Berechtigung für die Rolle, die liest, und Row Level Security mit einer Regel, egal wo sie liegt. Sie nach public zu verschieben spart dir also den Schritt der Freigabe und sonst nichts.

Wenn du sie verschiebst:

alter table billing.invoices set schema public;

Die Tabelle behält ihre Berechtigungen und ihre Regeln. Eine Funktion, deren Körper billing.invoices nennt, scheitert danach mit genau diesem Fehler, weil der Körper den Namen so behält, wie er geschrieben wurde.

Eine Tabelle, die nur Servercode braucht, kann auf einer Etage bleiben, die der Aufzug nie anfährt. Eine Funktion in public kann die eine Aufgabe damit erledigen, die deine App braucht, und eine Supabase-Funktion aufrufen zeigt, wie du Besuchern, die nicht angemeldet sind, das Recht execute wegnimmst.

Ursache 3: Eine Funktion mit leerem Suchpfad

Eine Funktion mit search_path = '' durchsucht gar keine Etage, also braucht jede Tabelle darin ihr Schema davor: public.invoices, nie ein nacktes invoices.

Der Security Advisor von Supabase verlangt genau diese Einstellung. Seine Warnung function_search_path_mutable sagt: "We recommend pinning functions' search_path to an empty string". Dieselbe Seite endet mit: "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."

Die Warnung wird also mit einer Zeile behoben, und die Funktion scheitert danach am ersten nackten Tabellennamen:

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;
$$;

Schreib from public.invoices, und sie funktioniert wieder. Behalte die Einstellung: Sie sorgt dafür, dass niemand, der die Funktion aufruft, einen nackten Namen auf eine eigene Tabelle umlenken kann.

Aus deiner App mit .rpc() aufgerufen, geht der Fehler als 42P01 mit HTTP 404 durch PostgREST, dem Status, den der Code von PostgREST einer unbekannten Tabelle gibt. Ein Trigger bei neuen Registrierungen, der so scheitert, zeigt sich als "Database error saving new user".

Mit leerem Suchpfad gibt es keine Etage zum Durchsuchen, also findet nur eine vollständige Adresse den Raum.

Ursache 4: Die Tabelle ist noch nicht in diesem Projekt

Die Tabelle existiert in einer anderen Datenbank oder nur in deinem Code.

Ein Supabase-Konto kann mehrere Projekte enthalten, und ein Branch ist ein eigenes Projekt mit eigener Datenbank. Eine Tabelle, die im SQL Editor des falschen Projekts angelegt wurde, ist ein Raum in einem anderen Gebäude. Die Spaltenversion dieses Fehlers zeigt, wie du die Projektreferenz in der Supabase-Adresse deiner App mit der im Dashboard vergleichst.

Wenn du mit Lovable baust, such im Chat nach SQL, das du freigeben solltest. Die Supabase-Dokumentation von Lovable sagt, Lovable "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Bis du freigibst, existiert die Tabelle nur im Code, der sie liest.

Eine Tabelle verschieben, von der deine App abhängt

Behalte eine Kopie der Datenbank von vor dem Verschieben, und erfahre es, wenn sich etwas ändert, das deine App preisgibt.

Reeve Care bewahrt eine Kopie deiner Supabase-Datenbank auf:

  • eine verschlüsselte Kopie nach Zeitplan, auf Reeves eigenem Speicher in der EU, außerhalb deines Supabase-Kontos
  • jedes Schema, das du angelegt hast, public und die anderen, mit ihren Tabellen und Zeilen, wie sie vor deiner Änderung waren
  • jede Kopie wird geprüft, bevor sie zählt, indem die Zeilen in jeder Tabelle gezählt werden
  • auch deine hochgeladenen Dateien, sobald du eine Storage-Zugangsberechtigung verbindest
  • Wiederherstellung mit einem Klick, vorher wird eine Sicherheitskopie des aktuellen Stands angelegt
  • alles, was Monitor macht

Reeve Monitor prüft weiter von außen:

  • ob deine App erreichbar ist, jede Minute geprüft
  • der vollständige Sicherheitscheck jede Stunde, für bis zu drei Apps
  • eine Nachricht, wenn sich ein Ergebnis ändert, sodass ein Deploy, der ändert, was deine App preisgibt, dich erreicht, ohne dass du nachsehen musst
  • ein monatlicher Bericht in klarer Sprache über das, was er gesehen hat

Was du jetzt tun solltest

Was zu tun ist

  • Lies die Meldung: Der Name in Anführungszeichen ist das, wonach gefragt wurde, und public. davor ist das Schema, das durchsucht wurde.
  • Vergleich den Namen mit dem Table Editor, samt Großbuchstaben. Eine Tabelle mit einem Großbuchstaben im Namen braucht in SQL doppelte Anführungszeichen.
  • Finde mit der Abfrage auf information_schema.tables heraus, in welchem Schema die Tabelle liegt, und ruf eine Tabelle außerhalb von public mit .schema('name').from(...) auf.
  • Bevor du ein Schema freigibst, führ die Abfrage auf pg_tables aus und schalte Row Level Security für jede Tabelle ein, bei der false steht.
  • In einer Funktion mit search_path = '' schreibst du jede Tabelle als public.name.
  • Prüf, dass das Projekt, das deine App aufruft, das ist, in dem du die Tabelle angelegt hast.

Fang bei den beiden Namen in der Meldung an, der Tabelle und dem Schema davor, denn sie sagen genau, nach welcher Tür auf welcher Etage gefragt wurde. Bevor du etwas verschiebst, umbenennst oder freigibst, das Menschen benutzen, behalte eine Kopie der Datenbank von vor der Änderung.

FAQ

Warum sagt Supabase, dass es meine Tabelle nicht gibt?

Weil in dem Schema, das die Abfrage durchsucht hat, keine Tabelle mit genau diesem Namen liegt. Prüf vier Dinge der Reihe nach: die Großbuchstaben im Namen, das Schema, in dem die Tabelle liegt (deine App sucht in public, solange du nichts anderes sagst), das Projekt, mit dem deine App spricht, und ob die Abfrage in einer Funktion mit leerem search_path läuft, wo jede Tabelle ihr Schema davor braucht.

Was bedeutet "the schema must be one of the following"?

Das ist der Fehler PGRST106 im Wortlaut älterer PostgREST-Versionen. Deine App hat ein Schema angefragt, das das Projekt nicht freigibt, zum Beispiel über .schema('name'), und die Liste danach sind die freigegebenen Schemas. Neuere Versionen sagen "Invalid schema: name" und setzen dieselbe Liste in den Hinweis. Das Schema freizugeben behebt es, und das lohnt sich bewusst zu entscheiden.

Ist es sicher, in Supabase ein Schema freizugeben?

Erst wenn jede Tabelle darin Row Level Security eingeschaltet und eine Regel geschrieben hat. Supabases Anleitung zu eigenen Schemas gibt anon, der Rolle jedes Besuchers, der nicht angemeldet ist, vollen Zugriff auf jede Tabelle im Schema, auch auf Tabellen, die später dort angelegt werden. Seine Sicherheitsanleitung ergänzt, dass Tabellen aus dem SQL Editor Row Level Security nicht von selbst bekommen.

Spielt Groß- und Kleinschreibung bei Tabellennamen in Supabase eine Rolle?

Ja. Ein Name, der in SQL ohne doppelte Anführungszeichen geschrieben wird, wird kleingeschrieben, während der Table Editor und supabase-js die Großbuchstaben genau so behalten, wie sie geschrieben wurden. Eine Tabelle, die als Invoices angelegt wurde, schreibst du in SQL als "Invoices", mit Anführungszeichen, und in deiner App als .from('Invoices').

Soll ich die Tabelle stattdessen nach public verschieben?

Das Verschieben behebt die Schema-Hälfte des Fehlers und nichts an der Sicherheit: Eine Tabelle, die deine App aus dem Browser liest, braucht eine Berechtigung für die Rolle, die liest, und Row Level Security mit einer Regel, egal wo sie liegt. Eine Tabelle, die nur Servercode braucht, kann in einem nicht freigegebenen Schema bleiben, erreicht über eine Funktion in public, die genau die eine Aufgabe erledigt, die deine App braucht.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Sichere deine Supabase-Datenbank vor der nächsten Änderung

Reeve sichert sie verschlüsselt nach Zeitplan und stellt sie für dich wieder her, wenn etwas schiefgeht.

Supabase-Backups ansehen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.