Sicherheitsgrundlagen
"Database error saving new user" in Supabase: wo du nachsiehst
"Database error saving new user" heißt: etwas an deiner Supabase-Registrierung ist fehlgeschlagen. Der echte Fehler steht in deinen Logs.

Kurz gesagt
- "Database error saving new user" heißt, dass Supabase die Registrierung angenommen hat, etwas daran fehlgeschlagen ist und das Ganze wieder zurückgenommen wurde. Es wurde kein Konto angelegt.
- Die Meldung ist absichtlich nichtssagend, und die echte steht längst irgendwo. Supabase schickt dich in der eigenen Fehlerhilfe erst in die Auth-Logs und dann in die Postgres-Logs für die Zeile darunter.
- Fast immer ist es ein Trigger auf auth.users, der die Tabelle nicht erreicht, in die er schreibt. Die Lösung ist eine security definer Funktion, und worauf es ankommt, ist, wie viel diese Funktion darf.
Jemand will ein Konto in deiner App anlegen und bekommt eine Antwort, in der es gar nicht um ihn geht:
Database error saving new user
Niemand kann sich registrieren. Das Formular lief, als du es gebaut hast, dein Projekt ist oben, und das Einzige, was du hast, ist ein Satz ohne Tabelle, ohne Spalte und ohne Grund.
Hier ist der Punkt, den ein Guide nach dem anderen falsch macht: diese Meldung wurde gebaut, um dir nichts zu sagen, und dein echter Fehler steht längst woanders geschrieben. Such den Satz, und man reicht dir einen Block SQL zum Einsetzen, meistens irgendwessen Version einer Funktion, die in eine Tabelle mit fremden Spaltennamen schreibt. Das klappt entweder, oder du hast jetzt zwei Probleme statt einem. Die Zeile, die sagt, was schiefging, liegt in den Logs deines eigenen Projekts, und sie nennt die Tabelle.
Was "Database error saving new user" bedeutet
Supabase hat das neue Konto angenommen und dann wieder zurückgenommen, weil etwas, das an der Registrierung hängt, fehlgeschlagen ist.
Eine Registrierung schreibt eine Zeile, in eine Tabelle namens auth.users, die
Supabase für dich verwaltet. In den meisten Projekten hängt an diesem
Schreibvorgang noch etwas: ein Trigger, der die neue Person in eine eigene
profiles-Tabelle kopiert, ein Constraint, ein Fremdschlüssel. Wirft dieses
angehängte Etwas einen Fehler, rollt die Datenbank den gesamten Vorgang zurück,
und dazu gehört die Zeile in auth.users. Es bleibt kein halbfertiges Konto
übrig. Es steht aber auch nichts in der Antwort, was dir sagt, welches angehängte
Etwas es war.
Stell dir einen Tresen mit einem Hinterzimmer vor. Jemand trägt sich vorn in das Buch ein, und ein Angestellter nimmt den neuen Namen mit nach hinten, um ihn in ein zweites Buch zu übertragen. Kommt der Angestellte nicht in den Raum, in dem dieses zweite Buch liegt, dann gilt die Anmeldung nicht: die Zeile vorn wird durchgestrichen, und der Person wird gesagt, es habe ein Problem gegeben. Was ihr niemand sagt, ist, dass das Problem eine verschlossene Tür zwölf Meter weiter war.
Die Unschärfe ist Absicht, und sie ist richtig so. Dein Registrierungsformular steht offen im Internet. Eine Meldung, die deine Tabellen benennt, würde jedem deine Datenbank beschreiben, der bereit ist, eine E-Mail-Adresse einzutippen.
Lautet die Antwort stattdessen email rate limit exceeded, liegt es nicht an
deiner Datenbank: Supabase hat sich geweigert, die Bestätigungsmail zu
verschicken, und
dieser Fehler hat eine eigene Seite.
Wo steht der echte Fehler?
In den Logs deines Projekts, in zwei Schritten, und Supabase gibt in der eigenen Fehlerhilfe die Reihenfolge vor.
Reproduzier ihn einmal, bevor du suchst, damit du eine Uhrzeit zum Abgleichen hast. Versuch eine Registrierung in deiner Live-App, lass sie scheitern und merk dir den Zeitpunkt.
Dann im Supabase-Dashboard:
- Öffne Logs und lies die Auth-Logs. Such den Eintrag zu dieser Uhrzeit. Er trägt den Fehler, den Auth selbst aufgeschrieben hat, und der ist genauer als der Satz, den deine App bekommen hat.
- Zeigt dieser Eintrag auf die Datenbank, geh zurück zu Logs und öffne die Postgres-Logs. Die Zeile zum selben Zeitpunkt ist dein echter Fehler, geschrieben im gewöhnlichen Vokabular von Postgres: eine Spalte, eine Relation, ein Typ, eine Berechtigung.
Supabase nennt in der Fehlerhilfe zu dieser Meldung drei Ursachen. Du hast einen
Trigger oder eine Trigger-Funktion auf der Tabelle auth.users, die einen Fehler
hat. Du hast auf auth.users ein Constraint gelegt, das nicht erfüllt wird. Oder
du benutzt Prisma, und es hat die Berechtigungen auf auth.users kaputt gemacht.
Aus derselben Notiz stammt auch der zweistufige Weg durch die Logs oben.
Bei einer App aus Lovable, Bolt, Replit oder v0 deckt die erste Ursache die
meisten Fälle ab, denn eine neue Registrierung in eine profiles-Tabelle zu
kopieren ist das Muster, zu dem jeder Builder greift, und ein Trigger ist, wie es
gemacht wird.
Ursache 1: Der Trigger erreicht deine Tabelle nicht
Die Datenbankrolle, die deinen Trigger ausführt, darf nicht aus dem
auth-Schema heraus, und dein Trigger schreibt genau dorthin.
Supabase erklärt das auf dem eigenen Diskussionsboard, und die Formulierung lohnt
sich genau. Die Datenbankrolle, die die Authentifizierung verwaltet,
supabase_auth_admin, hat nur die Berechtigungen, die sie für ihre eigenen
Aufgaben braucht, und wenn ein von supabase_auth_admin ausgeführter Trigger mit
etwas außerhalb des auth-Schemas umgeht, gibt das einen Berechtigungsfehler.
Das ist der Ausweis des Angestellten. Er öffnet das vordere Büro, weil dort seine Arbeit liegt. Das zweite Buch liegt in einem Raum, für den der Ausweis nie ausgestellt wurde, und eine verschlossene Tür sagt nichts darüber, warum sie verschlossen ist.
Im Postgres-Log kommt das als Berechtigungsfehler an, der die Tabelle nennt, in die dein Trigger schreiben wollte.
Die Lösung, und was du damit abgibst
security definer an der Funktion, die sie mit den Rechten dessen laufen lässt,
der sie angelegt hat, statt mit denen dessen, der sie aufruft.
Supabase sagt es in einem Satz: eine security definer Funktion behält die Rechte
des Datenbankbenutzers, der sie angelegt hat, und solange das die Rolle
postgres ist, sollten deine Auth-Trigger mit Tabellen außerhalb umgehen können.
Der Angestellte trägt nicht mehr den eigenen Ausweis, sondern den des Chefs.
Das ist eine echte Übertragung von Befugnissen, also lohnt es sich zu wissen, was
du da hingeschrieben hast, bevor du es einsetzt. Jede Anweisung in dieser
Funktion läuft jetzt als Eigentümer. Supabase setzt im eigenen Beispiel den
search_path auf leer, mit security definer set search_path = '', und deshalb
nennt das Insert dort public.profiles vollständig statt nur profiles. Mit
leerem search_path löst ein nackter Tabellenname überhaupt nichts mehr auf. Einer
Funktion mit den Rechten des Eigentümers kann dann niemand still ihr Ziel gegen
eine andere Tabelle tauschen, je nachdem, was der search_path des Aufrufers
gerade sagt.
Was das nicht erlaubt, ist, beim nächsten abgelehnten Zugriff wieder nach
security definer zu greifen. Lass die Funktion eine Sache tun und zurückkehren,
so wie Supabase es vormacht.
Ursache 2: Eine Regel auf der Tabelle, in die du schreibst
Row Level Security auf deiner profiles-Tabelle kann das Insert ablehnen, und
die Registrierung scheitert dann auf genau dieselbe Weise.
Supabase sagt dir in der Anleitung zu diesem Muster, dass du Row Level Security für diese Tabelle anschalten sollst, und diese Anweisung ist richtig. Für deinen Trigger heißt das, dass das Insert erlaubt sein muss, entweder durch eine Policy, die es abdeckt, oder durch eine Funktion, die als jemand läuft, für den die Policies nicht gelten.
Hier hört das auf, ein Bug zu sein, und wird eine Entscheidung, die jemand um elf
Uhr abends trifft. Niemand kann sich registrieren, der Fehler sagt nichts, und im
Dashboard gibt es einen Schalter mit einer klar formulierten Beschriftung, der
den Fehler verschwinden lässt: Row Level Security auf profiles ausschalten. Der
nächste Versuch klappt. Was ebenfalls passiert, ist, dass eine Tabelle mit deinen
Nutzerinnen und Nutzern, mit allem, was du neben ihren Namen darin hältst, jedem
antwortet, der danach fragt.
Dieser Teil ist von außen sichtbar, und deshalb können wir ihn zählen. Unser Scanner liest Live-Apps so, wie es ein Fremder täte. Hinter 8.435 der Apps, die er benotet hat, haben wir ein Supabase-Projekt gefunden. Bei 3.680 davon hat die Frage "kann jemand ohne Konto diese Tabelle lesen" überhaupt eine brauchbare Antwort bekommen, und 2.096 haben mit Ja geantwortet. Bei 394 von ihnen war die offene Tabelle benannt wie eine Tabelle voller Menschen.
Ein Teil davon ist Absicht, denn eine veröffentlichte Speisekarte und eine Seite mit Anzeigen liegen in Tabellen, die ein Fremder lesen soll, und unser Scanner kann die nicht von einer Kundentabelle unterscheiden, die jemand geöffnet hat, um einen Fehler loszuwerden. Er berichtet, was geantwortet hat. Wenn du lieber deine eigene Antwort siehst, als darüber nachzudenken: unser kostenloser Scan liest deine Live-App und sagt dir, welche Tabellen geantwortet haben. Das dauert etwa 20 Sekunden und braucht kein Konto: App scannen.
Die Policy, die ein Trigger braucht, ist um einiges enger als die Regeln abzuschalten. Row Level Security für jede Tabelle anschalten zeigt, wie man eine schreibt, und "new row violates row-level security policy" ist die Meldung, die du bekommst, wenn eine Policy ein Insert ablehnt, das du mit eigenen Augen siehst.
Die anderen Ursachen, und wie jede im Log aussieht
Vier weitere, und die Zeile im Log unterscheidet sie in wenigen Worten.
Eine Tabelle, die es nicht gibt. Supabase deckt das mit ab: eine
Trigger-Funktion, die auf eine Tabelle zeigt, die nicht existiert oder gelöscht
wurde. Eine profiles-Tabelle, die gelöscht und unter anderem Namen neu gebaut
wurde, reißt jede Registrierung mit, und das Log nennt die fehlende Relation.
Ein Constraint auf auth.users. Ein Check oder ein Fremdschlüssel auf dieser
Tabelle, den die neue Zeile nicht erfüllt. Das lohnt sich auch dann zu suchen,
wenn du sicher bist, keinen Trigger angelegt zu haben, denn ein Fremdschlüssel
zwischen deiner eigenen Tabelle und dem Auth-Nutzer ist der häufigste Weg, auf
dem diese Ursache ankommt. Mit seiner Standardeinstellung lehnt derselbe
Fremdschlüssel später auch das Löschen dieser Person ab, und
diese Meldung hat einen eigenen Beitrag.
Ein Wert mit dem falschen Typ. Dieser taucht wortwörtlich auf. Ein Trigger,
der Registrierungsfelder aus raw_user_meta_data kopiert, liest sie mit ->>,
und ->> gibt Text zurück, wie der Wert beim Hineingehen auch ausgesehen haben
mag. Diesen Text in eine date-Spalte zu schreiben ergab in einem berichteten
Fall column "birth_date" is of type date but expression is of type text. Die
Reparatur ist ein Cast, mit der Extraktion zuerst in Klammern:
(new.raw_user_meta_data->>'birthDate')::date,
(new.raw_user_meta_data->>'height')::integer,
Von Hand eingefügte Zeilen. Supabase deckt auch Konten ab, die direkt per SQL
statt über die Auth-API angelegt wurden und bei denen Pflichtspalten im
auth-Schema als NULL stehen bleiben statt als leere Zeichenketten. Hat jemand
Nutzer mit einem insert eingespielt, ist das eine andere Reparatur als alle
oben.
Eine weitere Zeile, die dir dort begegnen kann:
duplicate key value violates unique constraint. Sie heißt, dass die Zeile, die
dein Trigger schreibt, schon da war, etwa weil deine App dasselbe Profil nach der
Registrierung noch einmal einfügt, und
dieser Fehler hat einen eigenen Beitrag.
Und noch eine: null value in column … violates not-null constraint. Dein Trigger
hat einen leeren Wert in eine Spalte geschrieben, die einen verlangt, meist ein
Registrierungsfeld, das das Formular nie geschickt hat, und
auch sie hat einen eigenen Beitrag.
Die letzte, die dir begegnen kann, ist
insert or update on table "profiles" violates foreign key constraint. Meist
heißt sie, dass der Trigger mit before insert angelegt wurde, sodass die Person,
für die er ein Profil schreibt, noch nicht gespeichert ist. Das Muster von
Supabase weiter unten nutzt after insert, und
diese Meldung hat einen eigenen Beitrag.
Die funktionierende Fassung, vollständig
Fang bei dem Muster an, das Supabase veröffentlicht, und nicht bei einem Suchergebnis, denn vier Dinge darin tragen die Last, und eine Paraphrase lässt meistens eines davon weg.
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
insert into public.profiles (id, first_name, last_name)
values (new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name');
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
Die vier sind security definer, der leere search_path, das vollständig
geschriebene public.profiles und das return new, mit dem die Registrierung
weiterläuft. Deine eigenen Spaltennamen kommen in das Insert, und jede Spalte,
die du dort nennst, muss existieren und muss annehmen, was ->> liefert.
Supabase stellt der Anleitung den Satz voran, den man wiederholen sollte: wenn der Trigger fehlschlägt, kann er Registrierungen blockieren, also teste deinen Code gründlich. Leg nach jeder Änderung daran ein echtes Konto in deiner Live-App an.
Wie deine Registrierung nach einer Änderung in Betrieb bleibt
Eine Registrierung ist der Teil deiner App, den du an dem Tag nicht mehr testest, an dem er läuft. Du hast dich einmal registriert, du hast ein Konto, und danach meldest du dich an. Der nächste Prompt, der deine Tabellen anfasst, kann die Registrierung für alle anderen kaputt machen, während deine eigene Sitzung sich völlig normal weiter verhält.
Reeve Monitor schaut für dich nach:
- alle neun Checks jede Stunde, für bis zu drei Apps
- eine E-Mail an dem Tag, an dem ein erneuter Scan deine App schlechter benotet als der vorige
- ob die App erreichbar ist, jede Minute
- ein monatlicher Bericht darüber, was gesehen wurde
Eine Regel, die zum Loswerden eines Fehlers abgeschaltet wurde, lässt eine Tabelle voller Menschen lesbar zurück. Reeve Care hält eine Kopie deiner Supabase-Datenbank dort, wo keine Änderung an deinen Regeln sie erreicht.
- eine verschlüsselte Kopie jeden Tag, außerhalb deines Supabase-Kontos aufbewahrt
- jede Kopie wird geprüft, bevor sie zählt, durch Zählen der Zeilen in jeder Tabelle
- eine Wiederherstellung per Klick, die sichert, was jetzt da ist, bevor sie es ersetzt
- deine hochgeladenen Dateien ebenfalls, sobald du eine Storage-Zugangsberechtigung verbindest
- alles, was Monitor tut
Was heute zu tun ist
Was zu tun ist
- Reproduzier eine fehlgeschlagene Registrierung und merk dir die Uhrzeit, bevor du irgendetwas änderst.
- Lies den Auth-Log-Eintrag zu diesem Zeitpunkt, und danach die Postgres-Log-Zeile zur selben Zeit, falls der Auth-Eintrag auf die Datenbank zeigt. Diese Zeile nennt die Spalte, die Relation oder die Berechtigung, an der es gescheitert ist.
- Prüf, ob deine Trigger-Funktion
security definerträgt und ihr Ziel vollständig nennt, alspublic.profilesstatt alsprofiles. - Lehnt eine Policy auf dieser Tabelle das Insert ab, schreib eine Policy dafür. Row Level Security abzuschalten löst den Fehler und lässt diese Tabelle für jeden lesbar, der danach fragt.
- Leg nach jeder Änderung an einem Registrierungs-Trigger ein echtes Konto in deiner Live-App an. Deine eigene Sitzung verbirgt eine kaputte Registrierung vollständig vor dir.
Fang bei der Tabelle an, in die dein Trigger schreibt, und lies dann die Policies auf jeder Tabelle, die du in derselben Woche angefasst hast. Die 10-Minuten-Sicherheits-Checkliste deckt ab, was in einer frisch gestarteten App sonst noch offen bleibt, und der Supabase-Sicherheitsguide geht den Rest davon durch, was ein Fremder erreichen kann.
FAQ
Was bedeutet "Database error saving new user"?
Es bedeutet, dass Supabase die neue Person in die eigene Tabelle auth.users geschrieben hat, dass etwas an diesem Schreibvorgang fehlgeschlagen ist und die Datenbank daraufhin den ganzen Vorgang samt der neuen Zeile zurückgenommen hat. Es wurde also kein Konto angelegt, und es liegt nichts Halbfertiges herum. Die Meldung nennt absichtlich keine Tabelle und keinen Grund, denn dein Registrierungsformular steht offen im Internet, und ein genauer Fehler würde jedem, der eine E-Mail-Adresse einträgt, deine Datenbank beschreiben.
Wo finde ich den echten Fehler?
In den Logs deines Projekts, und Supabase gibt in der eigenen Fehlerhilfe die Reihenfolge vor. Versuch eine Registrierung, lass sie scheitern und merk dir die Uhrzeit. Öffne Logs und lies den Auth-Eintrag zu diesem Zeitpunkt, der trägt das, was Auth selbst aufgeschrieben hat. Zeigt dieser Eintrag auf die Datenbank, öffne die Postgres-Logs zur selben Zeit: diese Zeile nennt die Spalte, die Relation oder die Berechtigung, an der es wirklich gescheitert ist.
Bricht Row Level Security auf meiner profiles-Tabelle die Registrierung?
Das kann sein. Schreibt dein Trigger in eine Tabelle mit Row Level Security und erlaubt keine Policy dieses Insert, wird es abgelehnt, und die Registrierung scheitert mit derselben nichtssagenden Meldung. Die Lösung ist eine Policy dafür oder eine Funktion, die mit Rechten läuft, für die die Policies nicht gelten. Die Regeln auf dieser Tabelle abzuschalten lässt den Fehler ebenfalls verschwinden, und danach ist eine Tabelle mit deinen Nutzerinnen und Nutzern für jeden lesbar, der danach fragt.
Was ist security definer, und ist das sicher?
Damit läuft die Funktion mit den Rechten dessen, der sie angelegt hat, statt mit denen dessen, der sie aufruft. Genau das lässt einen Trigger, den die Auth-Rolle ausführt, in eine Tabelle von dir schreiben. Es ist das Muster, das Supabase veröffentlicht, und es ist eine echte Übertragung von Befugnissen, deshalb sollte die Funktion genau eine enge Sache tun. Supabase setzt im eigenen Beispiel außerdem den search_path auf leer, und deshalb nennt das Insert dort public.profiles vollständig und nicht nur profiles.
Ich habe nie einen Trigger angelegt. Was kann es sonst sein?
Drei andere Dinge. Ein Constraint oder ein Fremdschlüssel auf auth.users, den die neue Zeile nicht erfüllt. Eine Trigger-Funktion, die auf eine Tabelle zeigt, die umbenannt oder gelöscht wurde. Und Supabase nennt Prisma ausdrücklich, weil es die Berechtigungen auf auth.users kaputt machen kann. Welches davon es ist, sagt dir die Zeile im Postgres-Log.