Zum Inhalt springen

Backups

"Database error deleting user" in Supabase und was mitgelöscht wird

"Database error deleting user" in Supabase heißt: Deine Datenbank hat das Löschen abgelehnt. Der Grund steht im Log, die Lösung entscheidet, was mitgeht.

Vlad Tkachenko11 Min. Lesezeit
Eine Mitgliederliste mit einer durchgestrichenen Zeile, dahinter zwei Bücher, aus denen die Zeilen mit derselben Marke gerissen sind.

Kurz gesagt

  • "Database error deleting user" in Supabase heißt, dass das Löschen deine Datenbank erreicht hat und abgelehnt wurde, also wurde nichts entfernt. Die Zeile mit dem Grund steht in deinen Postgres-Logs, und genau dorthin schickt dich Supabases eigene Troubleshooting-Notiz.
  • Steht in dieser Zeile, dass ein Foreign Key verletzt wird, dann hält eine Zeile in einer deiner Tabellen noch die ID des Nutzers. Postgres entfernt niemanden, auf den noch etwas zeigt, außer du hast festgelegt, was mit diesen Zeilen passieren soll.
  • Die erste Antwort, die du findest, ist on delete cascade. Sie löscht jede Zeile, die auf den Nutzer zeigt, und jede Zeile, die auf diese zeigt, bei jedem Löschen ab diesem Moment. Entscheide pro Tabelle und mach eine Kopie, bevor du eine Regel änderst.

Du öffnest Authentication → Users in deinem Supabase-Dashboard und löschst ein Konto. Vielleicht hat die Person darum gebeten, vielleicht ist es ein Testkonto aus der letzten Woche. Das Dashboard antwortet:

Failed to delete user: Database error deleting user

Das Konto steht weiter in der Liste. Die App, die du mit Lovable, Bolt, v0, Cursor, Claude Code oder Replit gebaut hast, läuft die ganze Zeit, und der Satz nennt weder eine Tabelle noch einen Grund.

Die erste Antwort, die du findest, ist eine Zeile SQL: Häng on delete cascade an das, was im Weg steht. Das funktioniert. Es legt aber auch fest, welche anderen Zeilen bei jedem künftig gelöschten Konto mitgelöscht werden, und danach fragt dich niemand mehr. Lies also zuerst den echten Fehler und triff diese Entscheidung dann Tabelle für Tabelle.

Was bedeutet "Database error deleting user" in Supabase?

Deine Datenbank hat das Löschen abgelehnt, und Supabase hat alles rückgängig gemacht, das Konto ist also genau so wie vorher.

Einen Nutzer zu löschen ist eine Aufgabe für Supabase Auth, den Dienst hinter dem Login deiner App. Im Quellcode von Auth läuft das Löschen in einer einzigen Transaktion, einer Gruppe von Änderungen, die entweder alle passieren oder gar keine, und ihr zentraler Schritt entfernt die Zeile der Person aus auth.users. Wenn irgendetwas in deiner Datenbank dagegen Einspruch erhebt, wird die Transaktion zurückgerollt, und Auth antwortet mit diesem einen Satz. Supabase führt ihn als HTTP 500 mit dem Code unexpected_failure, neben seinem Zwilling bei der Registrierung, "Database error saving new user".

Stell dir deine Datenbank als Verein vor. auth.users ist die Mitgliederliste, und jedes Mitglied hat eine Nummer: seine User-ID. Deine anderen Bücher schreiben diese Nummer auf ihre Zeilen, ob Profil, Bestellung, Beitrag oder Kommentar. Jemanden von der Liste zu streichen ist eine Zeile Arbeit. Nur trägt jedes andere Buch die Nummer weiter, und solange niemand aufgeschrieben hat, was mit diesen Zeilen geschehen soll, weigert sich der Schriftführer des Vereins.

Wo steht der echte Fehler?

In den Postgres-Logs deines Projekts, zu dem Zeitpunkt, an dem das Löschen fehlschlug.

Supabases Troubleshooting-Notiz zu den Fehlern bei der Nutzerverwaltung gibt den Weg vor. Versuch das Löschen einmal und notier dir die Uhrzeit. Dann im Dashboard:

  1. Öffne Logs und lies die Auth-Logs zu dieser Zeit. Der Eintrag ist Auths eigenes Protokoll des fehlgeschlagenen Löschens.
  2. Öffne die Postgres-Logs zur selben Zeit. Diese Zeile ist der echte Fehler, und sie nennt eine Tabelle, eine Regel oder eine Berechtigung.

Dieselbe Notiz nennt drei Ursachen, und die Logzeile sagt dir, welche du hast:

  • ein Trigger auf der Tabelle auth.users, der fehlschlägt
  • eine Constraint, die nicht erfüllt ist, was beim Löschen einen Foreign Key in einer anderen Tabelle bedeutet, der auf auth.users zeigt
  • Prisma, das laut der Notiz die Berechtigungen auf auth.users kaputtmachen kann

Eine Tabelle, die noch auf den Nutzer zeigt

Steht in der Postgres-Zeile violates foreign key constraint, dann hält eine Zeile in einer deiner Tabellen noch die ID dieses Nutzers, und Postgres entfernt niemanden, auf den noch etwas zeigt.

Supabases Notiz druckt die Form ab, die das annimmt:

ERROR:  23503: update or delete on table "users" violates foreign key constraint "profiles_id_fkey" on table "profiles"
DETAIL:  Key (id)=(7428a53c-75b7-4531-9ae9-1567d9c4ac0a) is still referenced from table "profiles".

Ein Foreign Key ist eine Regel auf einer Spalte, die besagt, dass jeder Wert darin in einer anderen Tabelle existieren muss. Hier heißt die Regel profiles_id_fkey, sie sitzt auf deiner Tabelle profiles, und die Zeile Key ist die ID des Nutzers, den du löschen wolltest. Im Verein ist das eine Zeile im Profilbuch mit der Nummer des Mitglieds, und der Schriftführer streicht es nicht, solange sie dort steht.

Dieselbe Regel lehnt auch von der anderen Seite ab. Eine neue Zeile, die einen Nutzer nennt, den es nicht gibt, kommt als insert or update on table … violates foreign key constraint zurück, und diese Meldung hat einen eigenen Beitrag.

Die Weigerung ist der Standard. Ein Foreign Key, der ohne Zusatz nach references auth.users angelegt wurde, bekommt das Verhalten, das Postgres no action nennt, und Supabases Anleitung zu Cascade Deletes sagt ausdrücklich, dass NO ACTION der Standard ist, wenn du nichts angibst. Eine Tabelle, die dein Builder so angelegt hat, funktioniert tadellos bis zu dem Tag, an dem zum ersten Mal jemand gelöscht wird.

Um jede Tabelle zu sehen, die auf deine Nutzer zeigt, und was jede beim Löschen tut, führ das im SQL Editor aus:

select conrelid::regclass as table_name,
       conname as rule,
       case confdeltype
         when 'c' then 'cascade'
         when 'n' then 'set null'
         when 'd' then 'set default'
         when 'r' then 'restrict'
         else 'no action'
       end as on_delete
from pg_constraint
where contype = 'f'
  and confrelid = 'auth.users'::regclass
  and connamespace <> 'auth'::regnamespace;

Die Abfrage liest nur den eigenen Katalog von Postgres und ändert nichts. Die letzte Zeile lässt Supabases eigene Tabellen im Schema auth weg. Jede Zeile mit no action oder restrict ist eine Tabelle, die das Löschen ablehnt, sobald sie eine Zeile für diesen Nutzer enthält.

Was löscht on delete cascade?

Jede Zeile, die auf den Nutzer zeigt, dann jede Zeile, die auf diese zeigt, bei jedem Löschen ab diesem Moment.

Es ist die Lösung, die man dir zuerst gibt, und Supabases Anleitung zur Nutzerverwaltung verwendet sie für eine Profiltabelle: Dort heißt es, man solle in der Referenz on delete cascade angeben. Für ein Profil stimmt das. Ein Profil beschreibt die Person und sonst nichts, also soll es gehen, wenn sie geht.

Schwierig wird es, wenn dieselben Worte auf jeder Tabelle landen. Supabases Cascade-Anleitung beschreibt das Verhalten schlicht: Wird eine Zeile aus der Elterntabelle gelöscht, werden alle zugehörigen Zeilen in den Kindtabellen ebenfalls gelöscht. Und es geht weiter nach unten. Im eigenen Beispiel der Anleitung leert ein einziges Löschen in der obersten Tabelle die beiden Tabellen darunter.

Im Verein sagt eine Kaskade dem Schriftführer, jede Zeile mit der Nummer des Mitglieds herauszureißen und danach jede Zeile in jedem anderen Buch, die auf eine herausgerissene Zeile zeigte. Stell dir einen Nutzer vor, der Beiträge geschrieben hat, auf die Kommentare anderer Leute zeigen. Mit Kaskade auf beiden entfernt das Löschen dieses einen Nutzers seine Beiträge und die Kommentare aller anderen darunter.

Eine Kaskade folgt jeder Regel, die auf die gelöschte Zeile zeigt, dann jeder Regel, die auf diese zeigt. Das letzte Buch verliert Zeilen, die anderen Leuten gehörten.

Nichts sagt dir, was mitgegangen ist. Auth beantwortet ein erfolgreiches Löschen mit einer leeren Antwort, das Dashboard zeigt ein Konto weniger, und die Zeilen, die im selben Moment gegangen sind, fehlen, wenn das nächste Mal jemand nach ihnen sucht.

Was jede Tabelle behält

Entscheide pro Tabelle, denn jede hält etwas anderes über die Person fest, die gegangen ist.

Was die Tabelle enthälton deleteWenn der Nutzer gelöscht wird
Nur die Person: ein Profil, ihre EinstellungencascadeDie Zeilen gehen mit
Was andere noch brauchen: ein Kommentar im Thread eines anderen, ein Zahlungsbelegset nullDie Zeilen bleiben, die Besitzerspalte ist leer
Alles, worum du dich zuerst selbst kümmern willstno action (der Standard)Das Löschen wird abgelehnt, solange diese Zeilen da sind

set null kommt mit zwei Bedingungen. Die Spalte muss leere Werte erlauben, sonst schlägt das Löschen erneut fehl, mit einer eigenen Meldung, null value in column … violates not-null constraint. Und eine Policy der Form user_id = auth.uid() zeigt diese Zeile nie wieder irgendwem, weil in SQL nichts gleich einem leeren Wert ist. Die Zeile bleibt erhalten, und deine App zeigt sie niemandem mehr.

Eine Regel zu ändern heißt, sie zu löschen und mit dem neuen Verhalten wieder anzulegen, so wie es Supabases Notiz empfiehlt:

alter table public.profiles
  drop constraint profiles_id_fkey,
  add constraint profiles_id_fkey
    foreign key (id) references auth.users (id) on delete cascade;

Nimm den Regelnamen, den die Abfrage oben ausgegeben hat, mit deiner eigenen Tabelle und Spalte.

Es gibt noch einen Weg, und der behält jede Zeile. Supabase Auth kann ein Konto weich löschen: auth.admin.deleteUser() nimmt die Option shouldSoftDelete, und damit behält Auth die Zeile in auth.users, markiert sie als gelöscht, verwürfelt E-Mail und Telefonnummer, leert Passwort und Metadaten und beendet die Sessions. Entfernt wurde nichts, also greift kein Foreign Key. Das läuft auf einem Server mit deinem Secret Key, nie im Browser.

Dasselbe Löschen unter drei Regeln. Die erste lehnt ab und ändert nichts, die zweite nimmt die zeigende Zeile mit, und die dritte behält sie mit einem leeren Platz.

Ein Trigger, der deine Tabelle nicht erreicht

Steht in der Postgres-Zeile permission denied for table, dann ist beim Löschen ein Trigger auf auth.users gelaufen und wollte eine Tabelle anfassen, in die er nicht darf.

Die Rolle, als die Supabase Auth arbeitet, supabase_auth_admin, hat nur die Berechtigungen, die ihre eigene Aufgabe braucht. Supabases Notiz sagt es so: Wenn ein Trigger, den supabase_auth_admin ausführt, außerhalb des Schemas auth arbeitet, entsteht ein Berechtigungsfehler. Ein Trigger, der nach einem gelöschten Konto aufräumt, etwa indem er in eine eigene Log-Tabelle schreibt, scheitert genau so, außer seine Funktion wurde mit security definer angelegt.

Die Reparatur ist dieselbe wie bei der Registrierung: eine Funktion mit security definer, angelegt von der Rolle postgres, die eine eng begrenzte Sache tut. Die Registrierungs-Variante dieses Fehlers geht sie Zeile für Zeile durch, samt dem leeren Suchpfad, der sie sicher hält.

Wenn deine App Prisma benutzt, lies zuerst die dritte Ursache in Supabases Notiz, denn sie nennt Prisma als einen Weg, auf dem die Berechtigungen auf auth.users kaputtgehen.

Was passiert mit den Dateien, die die Person hochgeladen hat?

In einem aktuellen Projekt geht das Löschen durch, und die Dateien bleiben, wo sie sind.

Supabases Anleitung zur Nutzerverwaltung warnt weiterhin, dass du keinen Nutzer löschen kannst, dem Objekte in Supabase Storage gehören. Das stimmte, solange storage.objects, die Tabelle, in der Storage für jede Datei eine Zeile führt, einen Foreign Key namens objects_owner_fkey auf auth.users hatte. Storage hat diesen Key inzwischen in seinen eigenen Schema-Migrationen entfernt (0017-drop-owner-foreign-key.sql), und seine Anleitung zum Besitz von Dateien sagt jetzt, dass der Besitz in einer Spalte owner_id steht. Listet die Abfrage weiter oben storage.objects mit objects_owner_fkey, hat dein Projekt noch die alte Regel, und die Warnung der Anleitung gilt für dich.

Sonst bleibt jede Datei, die die Person hochgeladen hat, nach ihrem Weggang in ihrem Bucket. Im Verein hängen die Mäntel weiter an ihren Haken, mit einer Nummernmarke, die niemand mehr hat. Eine Policy der Form owner_id = auth.uid()::text passt nie wieder auf sie, und eine Datei in einem öffentlichen Bucket wird weiter jedem ausgeliefert, der ihren Link hat. Ob ein Fremder die Dateien in einem deiner Buckets auflisten kann, ist einer der Checks im kostenlosen Scan von Reeve.

Um sie zu entfernen, geh über Storage selbst, über die Storage-Seite im Dashboard oder die Storage-API. Ein einfaches SQL-Delete auf seinen Tabellen wird abgelehnt, und Storage sagt, warum: "Direct deletion from storage tables is not allowed. Use the Storage API instead." Was eine Datenbankkopie von diesen Dateien enthält und was nicht, steht in Supabase Storage sichern.

Mach eine Kopie, bevor du einen Foreign Key änderst

Postgres hat keinen Papierkorb, und eine Kaskade entfernt Zeilen aus Tabellen, auf die du gar nicht geschaut hast.

Was eine Kaskade oder ein set null ändert, ist endgültig, sobald das Löschen gelingt, und der einzige Weg zurück ist eine Kopie von vorher. Supabases Free-Plan behält gar keine Backups. Pro behält 7 Tage tägliche Kopien, Team 14 und Enterprise bis zu 30, und jede davon liegt im selben Supabase-Projekt. Deine Datenbank selbst zu kopieren braucht ein paar Befehle, und eine Kopie mit deinen Nutzern braucht den dritten der drei Befehle, die Supabase veröffentlicht.

Probier die neuen Regeln dann an einem Testkonto aus, bevor ein echtes dran ist. Leg ein Konto an, gib ihm eine Zeile in jeder Tabelle, die die Abfrage gelistet hat, lösch es und schau nach, was in jeder Tabelle übrig ist.

Wenn dir lieber wäre, dass diese Kopie schon existiert, bevor jemand auf Löschen drückt, dafür gibt es Supabase-Backups von Reeve.

Ein Weg zurück, wenn ein Löschen mehr mitnimmt als gedacht

Eine Kaskade meldet nichts darüber, was sie entfernt hat. Du merkst es, wenn jemand fragt, wo seine Kommentare geblieben sind.

Reeve Care hält eine Kopie deiner Supabase-Datenbank von vor dem Löschen:

  • eine verschlüsselte Kopie nach Zeitplan, auf Reeves eigenem Speicher in der EU, außerhalb deines Supabase-Kontos
  • 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 Reeve Monitor tut: alle neun Checks deiner Live-App jede Stunde und eine Erreichbarkeitsprüfung alle 60 Sekunden

Was heute zu tun ist

Was zu tun ist

  • Versuch das Löschen einmal, notier die Uhrzeit und lies die Postgres-Logzeile von diesem Moment, bevor du irgendetwas änderst.
  • Nennt sie einen Foreign Key, führ die Abfrage oben aus, um jede Tabelle aufzulisten, die auf deine Nutzer zeigt, und was jede beim Löschen tut.
  • Entscheide pro Tabelle: cascade für Zeilen, die nur die Person beschreiben, set null für Zeilen, die andere noch brauchen, den Standard für alles, was du selbst wegräumen willst.
  • Mach eine Kopie der Datenbank, bevor du eine dieser Regeln änderst, und probier die neuen Regeln zuerst an einem Testkonto aus.
  • Entferne auch die Dateien der Person über Storage. In einem aktuellen Projekt geht das Konto, und die Dateien bleiben.

Wenn schon mehr Zeilen weg sind als gedacht, geht was du nach einem Löschen zurückbekommst durch, wo du in welcher Reihenfolge nachsiehst.

FAQ

Was bedeutet "Database error deleting user" in Supabase?

Supabase Auth wollte die Zeile der Person aus auth.users entfernen, etwas in deiner Datenbank hat das abgelehnt, und das ganze Löschen wurde rückgängig gemacht. Das Konto ist genau so wie vorher. Die Meldung nennt keine Tabelle, die Zeile, die eine nennt, steht im selben Moment in den Postgres-Logs deines Projekts.

Soll ich on delete cascade hinzufügen, um den Fehler zu beheben?

Auf einer Tabelle, die nur die Person beschreibt, etwa einer Profiltabelle, ja, und Supabases eigene Anleitung macht genau das. Auf einer Tabelle mit Dingen, die andere noch brauchen, etwa Kommentaren oder Bestellungen, entscheide vorher. Cascade löscht jede Zeile, die auf den Nutzer zeigt, und jede, die auf diese zeigt, bei jedem Löschen ab dann, und nichts meldet, was es entfernt hat.

Kann ich einen Nutzer löschen und seine Zeilen behalten?

Ja, auf zwei Wegen. Stell den Foreign Key auf on delete set null, dann bleiben die Zeilen mit leerer Besitzerspalte, wofür diese Spalte leere Werte erlauben muss. Oder ruf auth.admin.deleteUser() auf einem Server mit der Option shouldSoftDelete auf: Supabase Auth behält die Zeile, markiert sie als gelöscht, verwürfelt E-Mail und Telefonnummer und leert das Passwort, also wird nichts angefasst, was auf sie zeigt.

Warum sagt Supabase, dass ich einen Nutzer mit hochgeladenen Dateien nicht löschen kann?

Die Anleitung zur Nutzerverwaltung sagt das noch, und es stimmte, solange storage.objects einen Foreign Key namens objects_owner_fkey auf auth.users hatte. Supabase Storage hat diesen Key inzwischen in seinen eigenen Schema-Migrationen entfernt. In einem aktuellen Projekt geht das Löschen durch, und die Dateien bleiben in ihrem Bucket, unter einer owner_id, die kein Nutzer mehr hat.

Meldet das Löschen eines Nutzers ihn ab?

Nicht sofort. Laut Supabases Anleitung bleibt ein bereits ausgestellter Access Token gültig, bis er abläuft, auch nachdem das Konto gelöscht ist. Das Löschen entfernt die Sessions des Nutzers, die App bekommt also keinen neuen Token, aber der vorhandene funktioniert bis zu seiner Ablaufzeit.

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.