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.

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:
- Öffne Logs und lies die Auth-Logs zu dieser Zeit. Der Eintrag ist Auths eigenes Protokoll des fehlgeschlagenen Löschens.
- Ö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.userszeigt - Prisma, das laut der Notiz die Berechtigungen auf
auth.userskaputtmachen 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.
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ält | on delete | Wenn der Nutzer gelöscht wird |
|---|---|---|
| Nur die Person: ein Profil, ihre Einstellungen | cascade | Die Zeilen gehen mit |
| Was andere noch brauchen: ein Kommentar im Thread eines anderen, ein Zahlungsbeleg | set null | Die Zeilen bleiben, die Besitzerspalte ist leer |
| Alles, worum du dich zuerst selbst kümmern willst | no 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.
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:
cascadefür Zeilen, die nur die Person beschreiben,set nullfü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.