Saltar al contenido

Backups

"Database error deleting user" en Supabase, y qué se va con él

"Database error deleting user" en Supabase: tu base de datos rechazó el borrado. La causa está en tus logs y el arreglo decide qué más se borra.

Vlad Tkachenko12 min de lectura
Un registro de socios con una línea tachada y, detrás, dos libros más de los que se arrancaron las líneas con la misma ficha.

En resumen

  • "Database error deleting user" en Supabase significa que el borrado llegó a tu base de datos y fue rechazado, así que no se eliminó nada. La línea que dice la causa está en tus logs de Postgres, y la propia nota de troubleshooting de Supabase te manda allí.
  • Cuando esa línea dice que se viola una foreign key, una fila de alguna de tus tablas todavía guarda el id del usuario. Postgres no elimina a una persona mientras algo siga apuntando a ella, salvo que le hayas dicho qué hacer con esas filas.
  • La primera respuesta que encontrarás es on delete cascade. Borra cada fila que apunta al usuario, y cada fila que apunta a esas, en cada borrado a partir de ese momento. Decide tabla por tabla y haz una copia antes de cambiar una regla.

Abres Authentication → Users en tu panel de Supabase y borras una cuenta. Quizá la persona te pidió que la eliminaras, quizá es una cuenta de prueba que creaste la semana pasada. El panel responde:

Failed to delete user: Database error deleting user

La cuenta sigue en la lista. La app que hiciste con Lovable, Bolt, v0, Cursor, Claude Code o Replit ha funcionado todo el tiempo, y la frase no nombra ninguna tabla ni ningún motivo.

La primera respuesta que encontrarás es una línea de SQL: añade on delete cascade a lo que estorba. Funciona. También decide, para cada cuenta que se borre a partir de entonces, qué otras filas se borran con ella, y nadie vuelve a preguntarte por eso. Así que lee primero el error real y toma después esa decisión tabla por tabla.

¿Qué significa "Database error deleting user" en Supabase?

Tu base de datos rechazó el borrado y Supabase deshizo todo, así que la cuenta está exactamente como estaba.

Borrar un usuario es trabajo de Supabase Auth, el servicio que está detrás del inicio de sesión de tu app. En el código fuente de Auth, el borrado ocurre dentro de una sola transacción, un grupo de cambios que se aplican todos o ninguno, y su paso central quita la fila de la persona de auth.users. Si algo en tu base de datos se opone, la transacción se revierte y Auth responde con esta única frase. Supabase la recoge como HTTP 500 con el código unexpected_failure, junto a su gemela del registro, "Database error saving new user".

Piensa en tu base de datos como en un club de socios. auth.users es el registro de socios, y cada socio tiene un número: su id de usuario. Tus otros libros copian ese número en sus líneas, sea un perfil, un pedido, una publicación o un comentario. Tachar a alguien del registro es cosa de una línea. Pero cada libro sigue llevando el número, y mientras nadie haya escrito qué hacer con esas líneas, el secretario del club se niega.

¿Dónde está el error real?

En los logs de Postgres de tu proyecto, en el momento en que falló el borrado.

La nota de troubleshooting de Supabase sobre los errores al gestionar usuarios marca el camino. Intenta el borrado una vez y apunta la hora. Después, en el panel:

  1. Abre Logs y lee los logs de Auth a esa hora. La entrada es el registro que el propio Auth guardó del borrado fallido.
  2. Abre los logs de Postgres a la misma hora. Esa línea es el error real, y nombra una tabla, una regla o un permiso.

La misma nota da tres causas, y la línea del log te dice cuál tienes:

  • un trigger sobre la tabla auth.users que falla
  • una restricción que no se cumple, que en un borrado significa una foreign key de otra tabla que apunta a auth.users
  • Prisma, que según la nota puede romper los permisos sobre auth.users

Una tabla que todavía apunta al usuario

Si la línea de Postgres dice violates foreign key constraint, una fila de alguna de tus tablas todavía guarda el id de este usuario, y Postgres no elimina a una persona a la que algo sigue apuntando.

La nota de Supabase imprime la forma que toma:

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

Una foreign key es una regla sobre una columna que dice que cada valor que contiene debe existir en otra tabla. Aquí la regla se llama profiles_id_fkey, vive en tu tabla profiles, y la línea Key es el id del usuario que intentaste borrar. En el club, es una línea del libro de perfiles con el número del socio, y el secretario negándose a tacharlo mientras siga ahí.

La misma regla también rechaza desde el otro lado. Una fila nueva que nombra a un usuario que no existe vuelve como insert or update on table … violates foreign key constraint, y ese mensaje tiene su propio artículo.

La negativa es lo que viene por defecto. Una foreign key creada sin nada detrás de references auth.users recibe el comportamiento que Postgres llama no action, y la guía de Supabase sobre borrados en cascada lo dice con todas las letras: NO ACTION es el comportamiento por defecto si no especificas nada. Una tabla que tu builder creó así funciona perfectamente hasta la primera vez que se borra a alguien.

Para ver cada tabla que apunta a tus usuarios, y qué hace cada una en un borrado, ejecuta esto en el 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;

Solo lee el catálogo propio de Postgres y no cambia nada. La última línea deja fuera las tablas de Supabase en el esquema auth. Cada fila que diga no action o restrict es una tabla que rechazará el borrado siempre que tenga una fila de ese usuario.

¿Qué borra on delete cascade?

Cada fila que apunta al usuario, luego cada fila que apunta a esas, en cada borrado a partir de ese momento.

Es el arreglo que te dan primero, y la guía de gestión de usuarios de Supabase lo usa para una tabla de perfiles: indica que se especifique on delete cascade en la referencia. Para un perfil es lo correcto. Un perfil describe a la persona y nada más, así que debe irse cuando ella se va.

El problema empieza cuando esas mismas palabras van en todas las tablas. La guía de cascada de Supabase describe el comportamiento sin rodeos: cuando se borra una fila de la tabla padre, también se borran todas las filas relacionadas de las tablas hijas. Y sigue bajando. En el propio ejemplo de esa guía, un único borrado en la tabla de arriba vacía las dos tablas que tiene debajo.

En el club, una cascada le dice al secretario que arranque cada línea con el número del socio, y después cada línea de cualquier otro libro que apuntaba a una línea arrancada. Imagina un usuario que escribió publicaciones, con comentarios de otras personas que apuntan a ellas. Con cascada en ambas, borrar a ese único usuario elimina sus publicaciones y los comentarios de todos los demás en ellas.

Una cascada sigue cada regla que apunta a la fila borrada, y luego cada regla que apunta a esas. El último libro pierde filas que eran de otras personas.

Nada te dice qué se fue con él. Auth responde a un borrado correcto con una respuesta vacía, el panel muestra una cuenta menos, y las filas que se fueron en ese mismo momento faltan la próxima vez que alguien las busca.

Qué conserva cada tabla

Decide tabla por tabla, porque cada una guarda algo distinto sobre la persona que se fue.

Qué guarda la tablaon deleteCuando se borra el usuario
Solo a la persona: un perfil, sus ajustescascadeLas filas se van con él
Cosas que otros todavía necesitan: un comentario en el hilo de otro, un registro de pagoset nullLas filas se quedan, con la columna de dueño vacía
Cualquier cosa de la que quieras ocuparte tú primerono action (por defecto)El borrado se rechaza mientras queden esas filas

set null trae dos condiciones. La columna tiene que admitir valores vacíos, o el borrado vuelve a fallar con un mensaje propio, null value in column … violates not-null constraint. Y una política escrita como user_id = auth.uid() no volverá a mostrar esa fila a nadie, porque en SQL nada es igual a un valor vacío. La fila se conserva, y tu app deja de mostrársela a cualquiera.

Cambiar una regla significa quitarla y volver a añadirla con el comportamiento nuevo, que es lo que recomienda la nota 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;

Usa el nombre de regla que imprimió la consulta de arriba, con tu propia tabla y columna.

Hay una vía más, y conserva todas las filas. Supabase Auth puede hacer un borrado suave de una cuenta: auth.admin.deleteUser() acepta una opción shouldSoftDelete, y con ella Auth conserva la fila en auth.users, la marca como borrada, revuelve el email y el teléfono, vacía la contraseña y los metadatos, y cierra las sesiones. No se eliminó nada, así que no salta ninguna foreign key. Se ejecuta desde un servidor con tu clave secreta, nunca desde el navegador.

El mismo borrado con tres reglas. La primera se niega y no cambia nada, la segunda se lleva la línea que apunta al socio, y la tercera conserva esa línea con un hueco vacío.

Un trigger que no alcanza tu tabla

Si la línea de Postgres dice permission denied for table, un trigger sobre auth.users se ejecutó durante el borrado e intentó tocar una tabla en la que no tiene permiso para entrar.

El rol con el que trabaja Supabase Auth, supabase_auth_admin, solo tiene los permisos que su propio trabajo necesita. La nota de Supabase lo explica así: cuando un trigger que ejecuta supabase_auth_admin actúa fuera del esquema auth, provoca un error de permisos. Un trigger que limpia tras una cuenta borrada, por ejemplo escribiendo en una tabla de registro tuya, falla exactamente así salvo que su función se haya creado con security definer.

El arreglo es el mismo que en el registro: una función security definer, creada por el rol postgres, que hace una sola cosa concreta. La versión de este error en el registro la recorre línea a línea, incluido el search path vacío que la mantiene segura.

Si tu app usa Prisma, lee primero la tercera causa de la nota de Supabase, porque nombra a Prisma como una de las formas de romper los permisos sobre auth.users.

¿Qué pasa con los archivos que subió?

En un proyecto actual el borrado se completa, y los archivos se quedan donde están.

La guía de gestión de usuarios de Supabase todavía advierte que no puedes borrar a un usuario que es dueño de objetos en Supabase Storage. Era cierto mientras storage.objects, la tabla donde Storage guarda una fila por cada archivo, tenía una foreign key llamada objects_owner_fkey hacia auth.users. Storage ya quitó esa clave en sus propias migraciones de esquema (0017-drop-owner-foreign-key.sql), y su guía sobre la propiedad de archivos dice ahora que el dueño se guarda en una columna owner_id. Si la consulta de antes lista storage.objects con objects_owner_fkey, tu proyecto todavía tiene la regla antigua y la advertencia de la guía te afecta.

Si no, cada archivo que la persona subió se queda en su bucket cuando ella ya no está. En el club, los abrigos siguen colgados en sus perchas con una ficha que ya no tiene nadie. Una política escrita como owner_id = auth.uid()::text no volverá a coincidir con ellos, y un archivo de un bucket público se sigue sirviendo a cualquiera que tenga su enlace. Si un desconocido puede listar los archivos de uno de tus buckets es una de las comprobaciones del escaneo gratuito de Reeve.

Para quitarlos, ve a través del propio Storage, desde la página de Storage del panel o con la API de Storage. Un borrado SQL directo sobre sus tablas se rechaza, y Storage dice por qué: "Direct deletion from storage tables is not allowed. Use the Storage API instead." Qué guarda una copia de la base de datos de esos archivos, y qué deja fuera, está en cómo hacer backup de Supabase Storage.

Haz una copia antes de cambiar una foreign key

Postgres no tiene papelera, y una cascada borra filas de tablas que no estabas mirando.

Lo que cambia una cascada o un set null es definitivo en cuanto el borrado se completa, y la única vuelta atrás es una copia hecha antes. El plan gratuito de Supabase no guarda ningún backup. Pro guarda 7 días de copias diarias, Team 14 y Enterprise hasta 30, y todas viven dentro del mismo proyecto de Supabase. Copiar tu base de datos por tu cuenta lleva unos pocos comandos, y una copia que incluya a tus usuarios necesita el tercero de los tres comandos que publica Supabase.

Después prueba las reglas nuevas con una cuenta de prueba antes de tocar una real. Crea una cuenta, dale una fila en cada tabla que listó la consulta, bórrala y mira qué queda en cada tabla.

Si prefieres que esa copia ya exista antes de que alguien pulse borrar, para eso están los backups de Supabase de Reeve.

Una vuelta atrás cuando un borrado se lleva más de lo previsto

Una cascada no informa de nada de lo que eliminó. Te enteras cuando alguien pregunta dónde están sus comentarios.

Reeve Care guarda una copia de tu base de datos de Supabase de antes del borrado:

  • una copia cifrada según un calendario, guardada en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
  • cada copia verificada antes de contar, contando las filas de cada tabla
  • una restauración en un clic que guarda lo que hay ahora antes de reemplazarlo
  • también tus archivos subidos, en cuanto conectes una credencial de Storage
  • todo lo que hace Reeve Monitor: las nueve comprobaciones de tu app en producción cada hora, y una comprobación de disponibilidad cada 60 segundos

Qué hacer hoy

Qué hacer

  • Intenta el borrado una vez, apunta la hora y lee la línea del log de Postgres de ese momento antes de cambiar nada.
  • Si nombra una foreign key, ejecuta la consulta de arriba para listar cada tabla que apunta a tus usuarios y qué hace cada una en un borrado.
  • Decide tabla por tabla: cascade para filas que solo describen a la persona, set null para filas que otros todavía necesitan, el comportamiento por defecto para lo que quieras quitar a mano.
  • Haz una copia de la base de datos antes de cambiar cualquiera de esas reglas, y prueba las reglas nuevas primero con una cuenta de prueba.
  • Quita también los archivos de la persona a través de Storage. En un proyecto actual la cuenta se va y los archivos se quedan.

Si ya se han ido más filas de las que querías, qué puedes recuperar después de un borrado repasa dónde mirar, en orden.

Preguntas frecuentes

¿Qué significa "Database error deleting user" en Supabase?

Significa que Supabase Auth intentó quitar la fila de esa persona de auth.users, algo en tu base de datos se negó, y todo el borrado se deshizo. La cuenta queda exactamente como estaba. El mensaje no nombra ninguna tabla, y la línea que sí la nombra está en los logs de Postgres de tu proyecto en ese mismo momento.

¿Debo añadir on delete cascade para arreglarlo?

En una tabla que solo describe a la persona, como una tabla de perfiles, sí, y la propia guía de Supabase hace exactamente eso. En una tabla con cosas que otras personas todavía necesitan, como comentarios o pedidos, decide antes. Cascade borra cada fila que apunta al usuario y cada fila que apunta a esas, en cada borrado a partir de entonces, y nada te informa de lo que eliminó.

¿Puedo borrar un usuario y conservar sus filas?

Sí, de dos formas. Pon la foreign key en on delete set null y las filas se quedan con la columna de dueño vacía, lo que exige que esa columna admita valores vacíos. O llama a auth.admin.deleteUser() desde un servidor con la opción shouldSoftDelete: Supabase Auth conserva la fila, la marca como borrada, revuelve el email y el teléfono y vacía la contraseña, así que no se toca nada de lo que apunta a ella.

¿Por qué dice Supabase que no puedo borrar a un usuario que subió archivos?

Su guía de gestión de usuarios todavía lo dice, y era cierto mientras storage.objects tenía una foreign key llamada objects_owner_fkey hacia auth.users. Supabase Storage ya quitó esa clave en sus propias migraciones de esquema. En un proyecto actual el borrado se completa y los archivos se quedan en su bucket, con un owner_id que ya no tiene ningún usuario.

¿Borrar un usuario cierra su sesión?

No en el acto. Según la guía de Supabase, un token de acceso ya emitido sigue siendo válido hasta que caduca, incluso después de borrar la cuenta. Borrar al usuario elimina sus sesiones, así que la app no puede conseguir un token nuevo, pero el que ya tiene funciona hasta su hora de caducidad.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

Todos los artículos

Haz una copia de tu base de datos de Supabase antes del próximo cambio

Reeve la guarda cifrada con regularidad y la restaura por ti si algo falla.

Ver copias de Supabase

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.