Aller au contenu

Backups

"Database error deleting user" sur Supabase, et ce qui part avec

"Database error deleting user" sur Supabase : votre base a refusé la suppression. La cause est dans vos logs, et la solution décide de ce qui part avec.

Vlad Tkachenko12 min de lecture
Un registre de membres avec une ligne barrée et, derrière, deux autres livres dont les lignes portant le même jeton ont été arrachées.

En bref

  • "Database error deleting user" sur Supabase veut dire que la suppression a atteint votre base et a été refusée, donc rien n'a été supprimé. La ligne qui donne la cause est dans vos logs Postgres, et la note de troubleshooting de Supabase elle-même vous y envoie.
  • Quand cette ligne dit qu'une foreign key est violée, une ligne de l'une de vos tables contient encore l'id de l'utilisateur. Postgres ne supprime pas une personne tant que quelque chose pointe encore vers elle, sauf si vous lui avez dit quoi faire de ces lignes.
  • La première réponse que vous trouverez est on delete cascade. Elle supprime chaque ligne qui pointe vers l'utilisateur, puis chaque ligne qui pointe vers celles-là, à chaque suppression à partir de ce moment. Décidez table par table, et faites une copie avant de changer une règle.

Vous ouvrez Authentication → Users dans votre tableau de bord Supabase et vous supprimez un compte. Peut-être que la personne vous l'a demandé, peut-être que c'est un compte de test créé la semaine dernière. Le tableau de bord répond :

Failed to delete user: Database error deleting user

Le compte est toujours dans la liste. L'app que vous avez faite avec Lovable, Bolt, v0, Cursor, Claude Code ou Replit a marché tout du long, et la phrase ne nomme ni table ni raison.

La première réponse que vous trouverez est une ligne de SQL : ajoutez on delete cascade à ce qui bloque. Ça marche. Ça décide aussi, pour chaque compte supprimé à partir de là, quelles autres lignes partent avec lui, et personne ne vous reposera la question. Lisez donc d'abord la vraie erreur, puis prenez cette décision table par table.

Que veut dire "Database error deleting user" sur Supabase ?

Votre base a refusé la suppression, et Supabase a tout annulé, donc le compte est exactement comme avant.

Supprimer un utilisateur est le travail de Supabase Auth, le service derrière la connexion de votre app. Dans le code source d'Auth, la suppression se déroule dans une seule transaction, un groupe de modifications qui passent toutes ou pas du tout, et son étape centrale retire la ligne de la personne de auth.users. Si quoi que ce soit dans votre base s'y oppose, la transaction est annulée et Auth répond par cette seule phrase. Supabase la répertorie en HTTP 500 avec le code unexpected_failure, à côté de sa jumelle de l'inscription, "Database error saving new user".

Voyez votre base comme un club. auth.users est le registre des membres, et chaque membre a un numéro : son id d'utilisateur. Vos autres livres recopient ce numéro sur leurs lignes, que ce soit un profil, une commande, un article ou un commentaire. Rayer quelqu'un du registre tient en une ligne. Mais chaque autre livre porte encore le numéro, et tant que personne n'a écrit quoi faire de ces lignes, le secrétaire du club refuse.

Où est la vraie erreur ?

Dans les logs Postgres de votre projet, au moment où la suppression a échoué.

La note de troubleshooting de Supabase sur les erreurs de gestion des utilisateurs donne le chemin. Tentez la suppression une fois et notez l'heure. Puis, dans le tableau de bord :

  1. Ouvrez Logs et lisez les logs Auth à cette heure-là. L'entrée est le propre compte rendu d'Auth sur la suppression ratée.
  2. Ouvrez les logs Postgres à la même heure. Cette ligne est la vraie erreur, et elle nomme une table, une règle ou une permission.

La même note donne trois causes, et la ligne du log vous dit laquelle vous avez :

  • un trigger sur la table auth.users qui échoue
  • une contrainte non respectée, ce qui, pour une suppression, veut dire une foreign key d'une autre table qui pointe vers auth.users
  • Prisma, qui d'après la note peut casser les permissions sur auth.users

Une table qui pointe encore vers l'utilisateur

Si la ligne Postgres dit violates foreign key constraint, une ligne de l'une de vos tables contient encore l'id de cet utilisateur, et Postgres ne supprime pas une personne vers laquelle quelque chose pointe encore.

La note de Supabase imprime la forme que cela prend :

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".

Une foreign key est une règle posée sur une colonne, qui dit que chacune de ses valeurs doit exister dans une autre table. Ici la règle s'appelle profiles_id_fkey, elle vit sur votre table profiles, et la ligne Key donne l'id de l'utilisateur que vous vouliez supprimer. Dans le club, c'est une ligne du livre des profils qui porte le numéro du membre, et le secrétaire qui refuse de le rayer tant qu'elle s'y trouve.

La même règle refuse aussi dans l'autre sens. Une nouvelle ligne qui désigne un utilisateur inexistant revient avec insert or update on table … violates foreign key constraint, et ce message a son propre article.

Le refus est le comportement par défaut. Une foreign key créée sans rien après references auth.users reçoit le comportement que Postgres appelle no action, et le guide de Supabase sur les suppressions en cascade le dit en toutes lettres : NO ACTION est le comportement par défaut si vous ne précisez rien. Une table que votre builder a créée ainsi marche parfaitement jusqu'à la première fois où quelqu'un est supprimé.

Pour voir chaque table qui pointe vers vos utilisateurs, et ce que chacune fait lors d'une suppression, lancez ceci dans le SQL Editor :

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;

Elle lit seulement le catalogue de Postgres et ne change rien. La dernière ligne écarte les tables de Supabase dans le schéma auth. Chaque ligne qui affiche no action ou restrict est une table qui refusera la suppression dès qu'elle contient une ligne pour cet utilisateur.

Que supprime on delete cascade ?

Chaque ligne qui pointe vers l'utilisateur, puis chaque ligne qui pointe vers celles-là, à chaque suppression à partir de ce moment.

C'est la solution qu'on vous tend en premier, et le guide de gestion des utilisateurs de Supabase l'emploie pour une table de profils : il demande de préciser on delete cascade dans la référence. Pour un profil, c'est juste. Un profil décrit la personne et rien d'autre, il doit donc partir quand elle part.

Les ennuis commencent quand les mêmes mots se retrouvent sur toutes les tables. Le guide de Supabase sur la cascade décrit le comportement sans détour : quand une ligne est supprimée de la table parente, toutes les lignes liées des tables enfants sont supprimées aussi. Et ça continue vers le bas. Dans l'exemple même du guide, une seule suppression dans la table du haut vide les deux tables qui sont en dessous.

Dans le club, une cascade dit au secrétaire d'arracher chaque ligne portant le numéro du membre, puis chaque ligne de n'importe quel autre livre qui pointait vers une ligne arrachée. Imaginez un utilisateur qui a écrit des articles, avec les commentaires d'autres personnes qui pointent vers ces articles. Avec la cascade sur les deux, supprimer ce seul utilisateur retire ses articles et les commentaires de tous les autres dessus.

Une cascade suit chaque règle qui pointe vers la ligne supprimée, puis chaque règle qui pointe vers celles-là. Le dernier livre perd des lignes qui appartenaient à d'autres.

Rien ne vous dit ce qui est parti avec. Auth répond à une suppression réussie par une réponse vide, le tableau de bord affiche un compte de moins, et les lignes parties au même moment manquent la prochaine fois que quelqu'un les cherche.

Ce que chaque table garde

Décidez table par table, parce que chacune garde quelque chose de différent sur la personne qui est partie.

Ce que contient la tableon deleteQuand l'utilisateur est supprimé
Seulement la personne : un profil, ses réglagescascadeLes lignes partent avec lui
Ce dont d'autres ont encore besoin : un commentaire dans le fil d'un autre, un paiementset nullLes lignes restent, colonne de propriétaire vide
Tout ce dont vous voulez vous occuper vous-même d'abordno action (par défaut)La suppression est refusée tant qu'elles restent

set null vient avec deux conditions. La colonne doit accepter les valeurs vides, sinon la suppression échoue de nouveau avec un message à elle, null value in column … violates not-null constraint. Et une policy écrite user_id = auth.uid() ne montrera plus jamais cette ligne à personne, parce qu'en SQL rien n'est égal à une valeur vide. La ligne est conservée, et votre app ne la montre plus à qui que ce soit.

Changer une règle, c'est la retirer puis la remettre avec le nouveau comportement, comme le recommande la note de Supabase :

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

Prenez le nom de règle que la requête plus haut a affiché, avec votre propre table et votre propre colonne.

Il reste une voie, et elle garde toutes les lignes. Supabase Auth peut supprimer un compte en douceur : auth.admin.deleteUser() accepte une option shouldSoftDelete, et avec elle Auth garde la ligne dans auth.users, la marque comme supprimée, brouille l'email et le téléphone, efface le mot de passe et les métadonnées, et ferme les sessions. Rien n'a été retiré, donc aucune foreign key ne se déclenche. Cela s'exécute depuis un serveur avec votre clé secrète, jamais depuis le navigateur.

La même suppression sous trois règles. La première refuse et ne change rien, la deuxième emporte la ligne qui pointe vers le membre, et la troisième garde cette ligne avec un emplacement vide.

Un trigger qui n'atteint pas votre table

Si la ligne Postgres dit permission denied for table, un trigger sur auth.users s'est exécuté pendant la suppression et a voulu toucher une table où il n'a pas le droit d'entrer.

Le rôle sous lequel travaille Supabase Auth, supabase_auth_admin, n'a que les permissions dont son propre travail a besoin. La note de Supabase le dit ainsi : quand un trigger exécuté par supabase_auth_admin agit hors du schéma auth, il provoque une erreur de permission. Un trigger qui fait le ménage après un compte supprimé, par exemple en écrivant dans une table de journal à vous, échoue exactement ainsi, sauf si sa fonction a été créée avec security definer.

La réparation est la même que pour l'inscription : une fonction security definer, créée par le rôle postgres, qui fait une seule chose précise. La version inscription de cette erreur la détaille ligne par ligne, y compris le search path vide qui la garde sûre.

Si votre app utilise Prisma, lisez d'abord la troisième cause de la note de Supabase, car elle cite Prisma comme l'une des façons de casser les permissions sur auth.users.

Que deviennent les fichiers que la personne a envoyés ?

Sur un projet actuel, la suppression passe, et les fichiers restent où ils sont.

Le guide de gestion des utilisateurs de Supabase prévient toujours qu'on ne peut pas supprimer un utilisateur propriétaire d'objets dans Supabase Storage. C'était vrai tant que storage.objects, la table où Storage tient une ligne par fichier, avait une foreign key nommée objects_owner_fkey vers auth.users. Storage a depuis retiré cette clé dans ses propres migrations de schéma (0017-drop-owner-foreign-key.sql), et son guide sur la propriété des fichiers indique désormais que le propriétaire est dans une colonne owner_id. Si la requête plus haut liste storage.objects avec objects_owner_fkey, votre projet a encore l'ancienne règle et l'avertissement du guide vous concerne.

Sinon, chaque fichier envoyé par la personne reste dans son bucket après son départ. Dans le club, les manteaux restent aux patères avec un jeton que plus personne ne détient. Une policy écrite owner_id = auth.uid()::text ne leur correspondra plus jamais, et un fichier d'un bucket public est toujours servi à quiconque a son lien. Savoir si un inconnu peut lister les fichiers d'un de vos buckets fait partie des vérifications du scan gratuit de Reeve.

Pour les retirer, passez par Storage lui-même, depuis la page Storage du tableau de bord ou l'API Storage. Une suppression SQL directe sur ses tables est refusée, et Storage dit pourquoi : "Direct deletion from storage tables is not allowed. Use the Storage API instead." Ce qu'une copie de la base garde de ces fichiers, et ce qu'elle laisse de côté, est expliqué dans sauvegarder Supabase Storage.

Faites une copie avant de changer une foreign key

Postgres n'a pas de corbeille, et une cascade retire des lignes de tables que vous ne regardiez pas.

Ce qu'une cascade ou un set null modifie est définitif dès que la suppression réussit, et le seul chemin de retour est une copie faite avant. Le plan gratuit de Supabase ne garde aucune sauvegarde. Pro garde 7 jours de copies quotidiennes, Team 14 et Enterprise jusqu'à 30, et chacune vit dans le même projet Supabase. Copier votre base vous-même demande quelques commandes, et une copie qui inclut vos utilisateurs demande la troisième des trois commandes que publie Supabase.

Ensuite, essayez les nouvelles règles sur un compte de test avant un vrai. Créez un compte, donnez-lui une ligne dans chaque table que la requête a listée, supprimez-le et regardez ce qui reste dans chaque table.

Si vous préférez que cette copie existe déjà avant que quiconque appuie sur supprimer, c'est à ça que servent les sauvegardes Supabase de Reeve.

Garder un retour possible quand une suppression emporte plus que prévu

Une cascade ne signale rien de ce qu'elle a retiré. Vous l'apprenez quand quelqu'un demande où sont passés ses commentaires.

Reeve Care garde une copie de votre base Supabase d'avant la suppression :

  • une copie chiffrée selon un calendrier, conservée sur le stockage de Reeve dans l'UE, en dehors de votre compte Supabase
  • chaque copie vérifiée avant de compter, en comptant les lignes de chaque table
  • une restauration en un clic qui sauvegarde ce qui est là avant de le remplacer
  • vos fichiers téléversés aussi, dès que vous connectez un accès Storage
  • tout ce que fait Reeve Monitor : les neuf vérifications de votre app en ligne toutes les heures, et une vérification de disponibilité toutes les 60 secondes

Quoi faire aujourd'hui

Que faire

  • Tentez la suppression une fois, notez l'heure et lisez la ligne du log Postgres de ce moment-là avant de changer quoi que ce soit.
  • Si elle nomme une foreign key, lancez la requête plus haut pour lister chaque table qui pointe vers vos utilisateurs et ce que chacune fait lors d'une suppression.
  • Décidez table par table : cascade pour les lignes qui ne décrivent que la personne, set null pour celles dont d'autres ont encore besoin, le comportement par défaut pour tout ce que vous voulez retirer à la main.
  • Faites une copie de la base avant de changer l'une de ces règles, et essayez les nouvelles règles d'abord sur un compte de test.
  • Retirez aussi les fichiers de la personne via Storage. Sur un projet actuel, le compte part et les fichiers restent.

Si des lignes sont déjà parties plus loin que prévu, ce que vous pouvez récupérer après une suppression indique où regarder, dans l'ordre.

FAQ

Que veut dire "Database error deleting user" sur Supabase ?

Supabase Auth a voulu retirer la ligne de la personne de auth.users, quelque chose dans votre base a refusé, et toute la suppression a été annulée. Le compte est exactement comme avant. Le message ne nomme aucune table, et la ligne qui en nomme une se trouve dans les logs Postgres de votre projet au même moment.

Faut-il ajouter on delete cascade pour corriger ?

Sur une table qui ne décrit que la personne, comme une table de profils, oui, et le guide de Supabase fait exactement cela. Sur une table qui contient ce dont d'autres ont encore besoin, comme des commentaires ou des commandes, décidez d'abord. Cascade supprime chaque ligne qui pointe vers l'utilisateur et chaque ligne qui pointe vers celles-là, à chaque suppression à partir de là, et rien ne signale ce qui a été retiré.

Peut-on supprimer un utilisateur et garder ses lignes ?

Oui, de deux façons. Passez la foreign key en on delete set null, et les lignes restent avec une colonne de propriétaire vide, ce qui exige que cette colonne accepte les valeurs vides. Ou appelez auth.admin.deleteUser() depuis un serveur avec l'option shouldSoftDelete : Supabase Auth garde la ligne, la marque comme supprimée, brouille l'email et le téléphone et efface le mot de passe, donc rien de ce qui pointe vers elle n'est touché.

Pourquoi Supabase dit-il qu'on ne peut pas supprimer un utilisateur qui a envoyé des fichiers ?

Son guide de gestion des utilisateurs le dit encore, et c'était vrai tant que storage.objects avait une foreign key nommée objects_owner_fkey vers auth.users. Supabase Storage a depuis retiré cette clé dans ses propres migrations de schéma. Sur un projet actuel, la suppression passe et les fichiers restent dans leur bucket, sous un owner_id qu'aucun utilisateur n'a plus.

Supprimer un utilisateur le déconnecte-t-il ?

Pas tout de suite. D'après le guide de Supabase, un jeton d'accès déjà émis reste valable jusqu'à son expiration, même après la suppression du compte. Supprimer l'utilisateur efface ses sessions, donc l'app ne peut pas obtenir de nouveau jeton, mais celui qu'elle détient fonctionne jusqu'à son heure d'expiration.

Écrit par

Vlad Tkachenko

Fondateur de Reeve

Je passe mes journées à examiner des applications créées avec Lovable, Bolt, v0, Cursor et Replit, et la courte liste d'erreurs qui y reviennent sans cesse.

En savoir plus sur l'auteur

À lire ensuite

Tous les articles

Sauvegardez votre base Supabase avant la prochaine modification

Reeve en garde une copie chiffrée à intervalles réguliers et la restaure pour vous en cas de problème.

Voir les sauvegardes Supabase

Vérification externe automatisée, pas un audit complet. L'absence de problème n'est pas une garantie de sécurité.