Zum Inhalt springen

Sicherheitsgrundlagen

Supabase PGRST204: "Could not find the column in the schema cache"

Supabase "Could not find the column in the schema cache" heißt meist: Der Name passt nicht. Vier Prüfungen und der Einzeiler zum Neuladen.

Vlad Tkachenko11 Min. Lesezeit
Eine Hinweistafel im Foyer mit einem leeren Platz in ihrer Liste der Räume, davor ein Lieferschein mit einer bernsteinfarben markierten Zeile.

Kurz gesagt

  • Supabase "Could not find the column in the schema cache" (PGRST204) heißt: Deine App wollte ein Feld speichern, das die Tabelle nicht führt. PostgREST prüft jeden Schlüssel eines Insert oder Update gegen seine eigene Kopie deines Schemas, Buchstabe für Buchstabe.
  • Die übliche Ursache ist der Name: phoneNumber an eine Spalte namens phone_number geschickt, oder ein Schlüssel, der nie eine Spalte war, etwa verknüpfte Zeilen, die in einem Insert verschachtelt sind.
  • Supabase lädt diese Kopie von selbst neu, sobald sich eine Tabelle ändert. Ist sie wirklich veraltet, lädt NOTIFY pgrst, 'reload schema'; im SQL Editor sie neu, während die App weiterläuft.

Du hast deinen Builder gebeten, dem Profilformular eine Telefonnummer hinzuzufügen. Die Spalte ist im Table Editor zu sehen. Du drückst auf Speichern, nichts passiert, und die Browserkonsole zeigt das hier:

{
  "code": "PGRST204",
  "details": null,
  "hint": null,
  "message": "Could not find the 'phoneNumber' column of 'profiles' in the schema cache"
}

Supabase "Could not find the column in the schema cache" heißt, dass deine App ein Feld speichern wollte, das die Tabelle, so wie PostgREST sie sieht, nicht hat. PostgREST ist der Teil von Supabase, der die Aufrufe supabase.from(...) deiner App in Datenbankabfragen übersetzt. Es führt eine eigene Liste aller Tabellen und Spalten, den Schema-Cache, damit es die Datenbank nicht bei jeder Anfrage nach ihrem Aufbau fragen muss.

Hier liegt der Teil, den die meisten Antworten falsch machen: Sie sagen dir, du sollst den Schema-Cache neu laden. Bei Supabase lädt sich der Cache jedes Mal selbst neu, wenn sich eine Tabelle ändert. Wenn du also gerade eine Spalte angelegt hast und diesen Fehler trotzdem siehst, ist der Cache der unwahrscheinlichste Grund. Meistens ist es der Name.

Was bedeutet "Could not find the column in the schema cache"?

Ein Schlüssel in einem Schreibvorgang deiner App hat keine passende Spalte in dieser Tabelle.

Stell dir den Schema-Cache als Hinweistafel im Foyer eines Gebäudes vor: eine Zeile für jedes Büro, also jede Tabelle, und darunter jeder Raum, also jede Spalte. Jede Anfrage deiner App ist ein Lieferschein. PostgREST liest jede Zeile des Lieferscheins gegen die Tafel, und ein Raum, den die Tafel nicht führt, wird schon am Empfang abgewiesen, bevor irgendetwas deine Daten erreicht.

Die Meldung nennt beide Seiten dieses Vergleichs. In Could not find the 'phoneNumber' column of 'profiles' in the schema cache ist phoneNumber das, was deine App geschickt hat, und profiles die Tabelle, an die es ging. Drei weitere Dinge stehen fest im eigenen Code von PostgREST:

  • der Code ist PGRST204 und der Status HTTP 400, die Anfrage selbst war also falsch
  • er kommt nur von Schreibvorgängen: .insert(), .update() und .upsert(). Ein Lesevorgang, der eine fehlende Spalte nennt, scheitert mit einem anderen Fehler, den Postgres selbst schreibt
  • er bringt keinen Hinweis mit. Bei einer fehlenden Tabelle schlägt PostgREST den nächstliegenden Namen vor, den es kennt; bei einer fehlenden Spalte ist hint gleich null, und den Vergleich machst du

Jeder Schlüssel im Objekt, das du einem dieser drei Aufrufe übergibst, muss eine Spalte dieser Tabelle sein, so geschrieben, wie die Tabelle sie schreibt.

PostgREST vergleicht jeden Schlüssel, den deine App schickt, Buchstabe für Buchstabe mit den Spalten auf seiner Liste. phoneNumber und phone_number sind zwei verschiedene Namen.

Ursache 1: Der Name passt nicht, Buchstabe für Buchstabe

Die Prüfung ist exakt. Für PostgREST sind phoneNumber, phone_number und phonenumber drei verschiedene Spalten, und deine Tabelle hat höchstens eine davon.

Es gibt zwei übliche Wege hierher. Der erste sind zwei Benennungsgewohnheiten, die aufeinandertreffen: JavaScript schreibt ein Feld meist als phoneNumber, eine Postgres-Spalte heißt meist phone_number. Der Code, den dein Builder geschrieben hat, und das SQL, das er ausgeführt hat, können jeweils für sich stimmen und sich trotzdem widersprechen.

Der zweite ist die Falle mit den Anführungszeichen. In SQL wird ein Name ohne doppelte Anführungszeichen in Kleinbuchstaben umgewandelt. Die Dokumentation von Postgres sagt es klar: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case."

Was du im SQL Editor ausgeführt hastDie Spalte, die entstandWas deine App schicken muss
alter table profiles add column phone_number text;phone_numberphone_number
alter table profiles add column phoneNumber text;phonenumberphonenumber
alter table profiles add column "phoneNumber" text;phoneNumberphoneNumber

Die mittlere Zeile ist die, an der Leute hängen bleiben. Das SQL sieht aus, als hätte es phoneNumber angelegt, der Code schickt phoneNumber, und die Spalte heißt phonenumber.

Um die echten Namen zu sehen, öffne die Tabelle im Table Editor und lies die Spaltenköpfe, oder führ das hier im SQL Editor aus:

select column_name
from information_schema.columns
where table_schema = 'public' and table_name = 'profiles';

Dann sieh dir an, was deine App geschickt hat. Öffne in den Entwicklertools deines Browsers den Tab Netzwerk (Network), klick die fehlgeschlagene Anfrage an /rest/v1/profiles an und lies ihren Payload.

Die sicherere Lösung ist, die App zu ändern, weil das nichts an deinen Daten berührt. Bitte deinen Builder, überall dort, wo dieses Feld gespeichert wird, den exakten Namen der Spalte zu verwenden: "die Spalte in profiles heißt phone_number; nutze diesen Namen in jedem Insert und Update". Die Spalte stattdessen umzubenennen ist eine Änderung an einer Tabelle im laufenden Betrieb, und jeder andere Bildschirm, der noch den alten Namen nutzt, bricht beim ersten Aufruf.

Wenn du mit Lovable baust, geh den Chat zurück und such nach einer Datenbankänderung, die du freigeben solltest. Lovables Supabase-Dokumentation sagt, es "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Bis du sie freigibst, gibt es die Spalte nicht, egal was der Code erwartet.

Ursache 2: Ein Schlüssel, der nie eine Spalte war

Die Anfrage trägt etwas, das die Tabelle nie speichern sollte: verknüpfte Zeilen, die im Insert verschachtelt sind, oder ein Feld, das das Formular für sich selbst braucht.

Beim verschachtelten Insert erwarten die Leute, dass er funktioniert:

await supabase.from('orders').insert({
  customer_id: user.id,
  total: 42,
  order_items: [{ product_id: 7, quantity: 2 }],
})

Das scheitert mit Could not find the 'order_items' column of 'orders' in the schema cache. Dieselbe Form zu lesen funktioniert: .select('*, order_items(*)') liefert eine Bestellung samt ihren Positionen, also sieht es so aus, als müsste auch der umgekehrte Weg gehen. PostgREST liest jeden Schlüssel eines Schreibvorgangs als Spalte der einen Tabelle, für die die Anfrage gilt. Ein Supabase-Nutzer meldete es als Bug, und die Antwort, bevor das Issue am 18. Oktober 2026 geschlossen wurde, lautete, dass verschachtelte Inserts eine fehlende Funktion sind. Der Wunsch danach steht seit Februar 2017 offen im Tracker von PostgREST.

Zwei weitere Formen desselben Problems:

  • eine Zeile, die zusammen mit ihren verknüpften Zeilen gelesen, in einem Formular bearbeitet und als Ganzes per .update() zurückgeschickt wird, sodass das angehängte order_items oder profiles mitgeht
  • der gesamte Zustand eines Formulars, direkt an .insert() übergeben, mit Feldern, die das Formular braucht und die Tabelle nicht, etwa confirmPassword

Die Lösung: Schreib jede Tabelle für sich, die Elterntabelle zuerst. Leg die Bestellung an, lass dir ihre id mit .select('id').single() zurückgeben und leg dann die Positionen mit dieser order_id an. Das sind zwei Anfragen, wenn also die zweite scheitert, ist die Bestellung trotzdem ohne ihre Positionen gespeichert. Wenn das nie passieren darf, pack beide Inserts in eine Postgres-Funktion und ruf sie mit .rpc() auf, was die Antwort in jenem Issue das übliche Muster nennt. Dann wird alles gespeichert oder nichts. Antwortet dieser Aufruf stattdessen mit PGRST202, erklärt die Funktionsversion dieses Fehlers, warum.

Ein verschachtelter Lieferschein wird als eine weitere Spalte der ersten Tabelle gelesen. Getrennt verschickt, die Elterntabelle zuerst, findet jeder seine eigene Tafel.

Ursache 3: Die Änderung ging in ein anderes Projekt

Die Spalte existiert, in einer Datenbank, mit der deine App nie spricht. Ein Supabase-Konto kann mehrere Projekte enthalten, und ein Branch ist ein eigenes Projekt mit eigener Datenbank. Eine Spalte, die im SQL Editor des falschen angelegt wird, ändert eine Tafel in einem anderen Gebäude.

Im Issue #42183 von Supabase, einer Meldung, der Cache aktualisiere sich nicht ("does not update"), egal was man versuche, antwortete einer der Entwickler von PostgREST, dass die Meldung zwei verschiedene Projektreferenzen nannte, und bat darum zu bestätigen, dass die Änderung in dem Projekt gemacht wurde, an das die Anfragen gingen.

Die Prüfung braucht zwei Adressen. Deine App ruft https://<reference>.supabase.co auf, was du bei jeder Anfrage im Tab Netzwerk siehst. Das Dashboard zeigt das Projekt, in dem du bist, unter supabase.com/dashboard/project/<reference>. Die beiden Referenzen müssen dieselbe Zeichenkette sein. Welches Projekt ist meins erklärt, wie du es findest, wenn jemand anderes Supabase eingerichtet hat.

Ursache 4: Der Cache ist wirklich veraltet

Das ist bei Supabase der seltene Fall, weil Supabase den Cache für dich neu lädt. Jede Supabase-Datenbank wird mit einem Event-Trigger namens pgrst_ddl_watch eingerichtet, der nach jeder Änderung am Aufbau der Datenbank läuft, ALTER TABLE eingeschlossen, und NOTIFY pgrst, 'reload schema' sendet: die Nachricht, die PostgREST sagt, seine Liste neu aufzubauen. Du kannst ihn im Datenbank-Setup von Supabase nachlesen. Eine Spalte im Table Editor oder im SQL Editor anzulegen, beschriftet die Tafel von selbst neu.

Schief geht es, wenn diese Nachricht nie ankommt. Supabases Anleitung zur Fehlersuche beschreibt den Fall: "an underlying Postgres notification queue issue prevents PostgREST from receiving cache reload signals after schema changes". Das Log von PostgREST zeigt dann Failed listening for database notifications on the "pgrst" channel, und die Tafel bleibt, wie sie war, egal wie viele Spalten du anlegst.

Jedes ALTER TABLE bei Supabase sendet die Nachricht zum Neuladen von selbst. Ein veralteter Cache heißt, dass diese Nachricht nicht angekommen ist.

Wie lade ich den Supabase-Schema-Cache neu?

Führ eine Zeile im SQL Editor aus:

NOTIFY pgrst, 'reload schema';

Supabases Seite dazu sagt, du solltest unter Results "Success. No rows returned" sehen. Das ist bei einer laufenden App unbedenklich. Die Dokumentation von PostgREST sagt "Requests will wait until the schema cache reload is done", also sieht niemand, der die App gerade nutzt, währenddessen einen Fehler.

Ist die Benachrichtigungswarteschlange das Problem, bleibt die Nachricht zum Neuladen genauso hängen wie die automatische. Die Anleitung zur Fehlersuche nennt dafür eine zweite Zeile:

select pg_notification_queue_usage();

Laut Supabase bringt sie PostgREST dazu, seinen Cache zu aktualisieren, und "does not require a database instance or PostgREST service restart". Dann versuch das Speichern noch einmal.

Wenn Neuladen nicht hilft

Starte das Projekt neu, und wenn der Fehler auch das übersteht, geh zurück zu den Ursachen 1 bis 3.

Ein Neustart gibt PostgREST immer eine frische Liste. Derselbe Entwickler von PostgREST schrieb in #42183, dass ein neu gestartetes PostgREST "won't go live if it doesn't load a new schema cache". Restart project findest du unter Project Settings, dann General, und wie man ein Supabase-Projekt neu startet führt dich durch, samt der paar Minuten, die es offline ist.

Ein Fehler, der einen Neustart übersteht, kommt also von einer frischen, genauen Liste: Diese Tabelle in diesem Projekt hat keine Spalte mit diesem Namen. #42183 selbst, im Januar 2026 von jemandem eröffnet, der Neuladen, Neustart und das Löschen und Neuanlegen der Spalte versucht hatte, war noch offen, als wir es am 21. Oktober 2026 gelesen haben. Die Antwort des Entwicklers dort schlug außerdem vor, in den Infrastructure-Einstellungen des Projekts auf PostgREST 14 zu aktualisieren.

Streich Löschen und Neuanlegen von deiner Liste. alter table … drop column löscht jeden Wert, der in dieser Spalte gespeichert ist, und die neue Spalte mit demselben Namen beginnt leer. Wenn du gleich eine Tabelle ändern willst, in die echte Nutzer schreiben, behalte eine Kopie der Datenbank von vor der Änderung. Reeve Care legt eine nach Zeitplan an und bewahrt sie verschlüsselt auf Reeves eigenem Speicher in der EU auf.

Deine generierten Typen auf Stand halten

Generiere deine TypeScript-Typen nach jeder Änderung an einer Tabelle neu, dann kann ein falsch geschriebener Spaltenname schon im Editor auffallen, bevor er die Live-Seite erreicht.

Supabase baut die Datei aus deinen echten Spalten, entweder im Dashboard oder mit einem Befehl aus seiner Anleitung zum Generieren von Typen:

npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > database.types.ts

Wird der Client mit diesen Typen erstellt, createClient<Database>(…), führt der Typ Insert jeder Tabelle die Spalten auf, die .insert() annimmt. Eine falsch geschriebene Spalte in einem Objekt, das direkt im Aufruf ausgeschrieben ist, wird dann als Fehler markiert, bevor du veröffentlichst. Laut Lovables Dokumentation "regenerates the TypeScript types your app uses" es nach einer Datenbankänderung. Eine Typdatei, die vor deiner letzten Änderung an der Tabelle generiert wurde, führt noch die alten Spalten.

Eine Tabelle im laufenden Betrieb ändern, ohne zu verlieren, was drinsteht

Behalte eine Kopie von vor der Änderung und wisse sofort, ob die App danach noch antwortet.

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
  • die letzte Kopie vor deiner Änderung enthält die Tabellen und Spalten, wie sie waren, samt Zeilen
  • 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, sodass ein kaputter Speichern-Knopf und ein Ausfall leicht auseinanderzuhalten sind
  • 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 zwei Namen in der Meldung: den Schlüssel, den deine App geschickt hat, und die Tabelle, an die er ging.
  • Vergleich diesen Schlüssel Buchstabe für Buchstabe mit der Spalte im Table Editor. Ein Name, der in SQL ohne Anführungszeichen geschrieben wurde, wird in Kleinbuchstaben gespeichert.
  • Such nach Schlüsseln, die keine Spalten sind: verknüpfte Zeilen, die in einem Insert verschachtelt sind, oder eine ganze Zeile, die mit ihren verknüpften Zeilen zurückgeschickt wird. Schreib jede Tabelle für sich, oder pack beide Schreibvorgänge in eine Funktion.
  • Prüf, dass die Referenz in der Supabase-Adresse deiner App zu dem Projekt passt, das du geändert hast.
  • Passt das alles, führ NOTIFY pgrst, 'reload schema'; aus, dann select pg_notification_queue_usage();, und starte das Projekt nur neu, wenn keins von beiden hilft.
  • Generiere deine TypeScript-Typen nach jeder Änderung an einer Tabelle neu.

Fang mit dem Spaltennamen an, den die Meldung zitiert, denn er ist genau das, was deine App geschickt hat, und der Table Editor zeigt genau das, was die Tabelle hat. Bevor du in einer Tabelle, die Leute nutzen, etwas umbenennst oder löschst, behalte eine Kopie der Datenbank von vor der Änderung.

FAQ

Was bedeutet PGRST204?

PGRST204 ist der Code, den PostgREST, der Teil von Supabase, der die Datenbankaufrufe deiner App beantwortet, einem Schreibvorgang gibt, der eine Spalte nennt, die die Tabelle nicht hat. Er kommt mit HTTP 400 und der Meldung "Could not find the 'x' column of 'y' in the schema cache" zurück, wobei x der Schlüssel ist, den deine App geschickt hat, und y die Tabelle. Inserts, Updates und Upserts lösen ihn aus.

Ich habe die Spalte angelegt und es schlägt trotzdem fehl. Warum?

Meist liegt es am Namen. Prüf, dass deine App genau den gespeicherten Namen schickt (eine Spalte, die in SQL ohne Anführungszeichen als phoneNumber angelegt wurde, heißt gespeichert phonenumber), dass du sie in dem Projekt angelegt hast, das deine App aufruft, und dass sonst nichts in der Anfrage ein Schlüssel ist, den die Tabelle nicht kennt. Supabase lädt den Schema-Cache nach jedem ALTER TABLE von selbst neu, ein veralteter Cache ist also das Letzte, was du verdächtigen solltest.

Wie lade ich den Supabase-Schema-Cache neu?

Führ NOTIFY pgrst, 'reload schema'; im SQL Editor aus. Ändert das nichts, nennt Supabases Anleitung zur Fehlersuche einen zweiten Befehl, select pg_notification_queue_usage();, für den Fall, dass die Nachrichten zum Neuladen feststecken. Anfragen, die während des Neuladens ankommen, warten, bis es fertig ist.

Leert das Pausieren des Projekts den Cache?

Ja. PostgREST baut bei jedem Start einen frischen Schema-Cache, und ein pausiertes Projekt startet es beim Fortsetzen neu. Restart project unter Project Settings und dann General macht dasselbe ohne Pause. Übersteht der Fehler beides, lag der Cache die ganze Zeit richtig: Die Spalte fehlt in dieser Tabelle in diesem Projekt.

Warum scheitern verschachtelte Inserts mit diesem Fehler?

Weil PostgREST jeden Schlüssel eines Insert als Spalte einer einzigen Tabelle liest, und ein Schlüssel mit verknüpften Zeilen ist eine Spalte, die es nicht findet. Verknüpfte Zeilen in einer Anfrage zu lesen funktioniert, sie so zu schreiben wurde nie unterstützt. Leg zuerst den Elterndatensatz an und dann die Kinder mit seiner id, oder erledige beides in einer Postgres-Funktion, die du mit rpc() aufrufst.

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.