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.

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 lief | Was zurückkommt |
|---|---|
| Im SQL Editor, in einer Migration, einer Funktion oder einem Trigger | relation "invoices" does not exist, Code 42P01 |
| In deiner App, mit PostgREST 13 oder neuer | Could not find the table 'public.invoices' in the schema cache, Code PGRST205, HTTP 404 |
| In deiner App, mit einem älteren PostgREST | ein 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 freigibt | Invalid 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')nachinvoicesund nach nichts anderem.
| Wie die Tabelle angelegt wurde | In SQL schreibst du | In deiner App schreibst du |
|---|---|---|
create table invoices, oder Invoices in SQL ohne Anführungszeichen | invoices | .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
billingoderprivate, 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 inpublic - 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 inpublic, in ihrem Beispielprofiles, 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.
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.
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".
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,
publicund 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.tablesheraus, in welchem Schema die Tabelle liegt, und ruf eine Tabelle außerhalb vonpublicmit.schema('name').from(...)auf. - Bevor du ein Schema freigibst, führ die Abfrage auf
pg_tablesaus und schalte Row Level Security für jede Tabelle ein, bei derfalsesteht. - In einer Funktion mit
search_path = ''schreibst du jede Tabelle alspublic.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.