Перейти до вмісту

Backups

"Database error deleting user" у Supabase і що зникає разом

"Database error deleting user" у Supabase: ваша база відмовилася видаляти. Причина в логах, а виправлення вирішує, що ще буде видалено.

Vlad Tkachenko10 хв читання
Список членів клубу з одним закресленим рядком, а за ним ще дві книги, з яких вирвано рядки з тим самим жетоном.

Коротко

  • "Database error deleting user" у Supabase означає, що видалення дійшло до вашої бази й отримало відмову, тож нічого не видалено. Рядок із причиною лежить у ваших логах Postgres, і сама нотатка Supabase з troubleshooting відправляє вас саме туди.
  • Якщо в цьому рядку сказано, що порушено foreign key, то рядок в одній із ваших таблиць досі тримає id користувача. Postgres не видаляє людину, поки на неї ще щось вказує, хіба що ви сказали йому, що робити з тими рядками.
  • Перша відповідь, яку ви знайдете, це on delete cascade. Вона видаляє кожен рядок, що вказує на користувача, і кожен рядок, що вказує на ті, при кожному видаленні відтоді. Вирішуйте для кожної таблиці окремо й зробіть копію, перш ніж міняти правило.

Ви відкриваєте Authentication → Users у панелі Supabase і видаляєте обліковий запис. Можливо, людина сама попросила її видалити, можливо, це тестовий запис, який ви створили минулого тижня. Панель відповідає:

Failed to delete user: Database error deleting user

Запис досі в списку. Застосунок, який ви зробили в Lovable, Bolt, v0, Cursor, Claude Code чи Replit, весь цей час працював, а речення не називає ні таблиці, ні причини.

Перша відповідь, яку ви знайдете, це один рядок SQL: допишіть on delete cascade до того, що заважає. Це працює. А ще це вирішує, для кожного запису, видаленого відтоді, які інші рядки зникнуть разом із ним, і більше вас про це ніхто не спитає. Тож спершу прочитайте справжню помилку, а потім ухваліть це рішення окремо для кожної таблиці.

Що означає "Database error deleting user" у Supabase?

Ваша база відмовилася видаляти, і Supabase усе скасував, тож обліковий запис такий самий, як був.

Видалення користувача це робота Supabase Auth, сервісу, що стоїть за входом у ваш застосунок. У вихідному коді Auth видалення відбувається в одній транзакції, групі змін, які проходять або всі, або жодна, і її головний крок прибирає рядок людини з auth.users. Якщо щось у вашій базі проти, транзакцію відкочують, і Auth відповідає цим одним реченням. Supabase наводить його як HTTP 500 з кодом unexpected_failure, поруч із близнюком з реєстрації, "Database error saving new user".

Уявіть свою базу як клуб. auth.users це список членів, і в кожного члена є номер: його id користувача. Ваші інші книги переписують цей номер у свої рядки, чи то профіль, замовлення, допис або коментар. Викреслити когось зі списку справа одного рядка. Але кожна інша книга досі тримає номер, і поки ніхто не записав, що робити з тими рядками, секретар клубу відмовляє.

Де лежить справжня помилка?

У логах Postgres вашого проєкту, у ту мить, коли видалення не вдалося.

Нотатка Supabase з troubleshooting про помилки керування користувачами показує дорогу. Спробуйте видалити один раз і запишіть час. Потім у панелі:

  1. Відкрийте Logs і прочитайте логи Auth на цей час. Запис там це власний звіт Auth про невдале видалення.
  2. Відкрийте логи Postgres на той самий час. Цей рядок і є справжня помилка, і він називає таблицю, правило або дозвіл.

Та сама нотатка називає три причини, і рядок логу скаже, яка у вас:

  • тригер на таблиці auth.users, що падає
  • обмеження, яке не виконується, а при видаленні це означає foreign key в іншій таблиці, що вказує на auth.users
  • Prisma, який, за нотаткою, може зламати дозволи на auth.users

Таблиця, що досі вказує на користувача

Якщо в рядку Postgres написано violates foreign key constraint, то рядок в одній із ваших таблиць досі тримає id цього користувача, і Postgres не видаляє людину, на яку ще щось вказує.

Нотатка Supabase показує, як це виглядає:

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

Foreign key це правило на колонці, яке каже, що кожне значення в ній мусить існувати в іншій таблиці. Тут правило зветься profiles_id_fkey, воно стоїть на вашій таблиці profiles, а рядок Key це id користувача, якого ви намагалися видалити. У клубі це рядок у книзі профілів із номером члена, і секретар, який не викреслює його, поки той рядок на місці.

Те саме правило відмовляє й з іншого боку. Новий рядок, що називає користувача, якого не існує, повертається як insert or update on table … violates foreign key constraint, і цьому повідомленню присвячена окрема стаття.

Відмова і є поведінкою за замовчуванням. Foreign key, створений без нічого після references auth.users, отримує поведінку, яку Postgres називає no action, і посібник Supabase про каскадні видалення каже це прямо: NO ACTION діє за замовчуванням, якщо ви нічого не вказали. Таблиця, яку ваш білдер створив саме так, працює бездоганно до першого разу, коли когось видаляють.

Щоб побачити кожну таблицю, що вказує на ваших користувачів, і що кожна робить при видаленні, виконайте це в 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;

Запит лише читає власний каталог Postgres і нічого не змінює. Останній рядок відкидає службові таблиці Supabase у схемі auth. Кожен рядок із no action або restrict це таблиця, яка відмовить у видаленні щоразу, коли в ній є рядок цього користувача.

Що видаляє on delete cascade?

Кожен рядок, що вказує на користувача, потім кожен рядок, що вказує на ті, при кожному видаленні відтоді.

Це виправлення, яке вам дають першим, і посібник Supabase з керування користувачами застосовує його до таблиці профілів: там радять вказати on delete cascade у посиланні. Для профілю це правильно. Профіль описує людину й більше нічого, тож він має піти, коли йде вона.

Клопіт починається, коли ті самі слова потрапляють на кожну таблицю. Посібник Supabase про каскад описує поведінку прямо: коли з батьківської таблиці видаляють рядок, усі повʼязані рядки в дочірніх таблицях теж видаляються. І це йде далі вниз. У власному прикладі посібника одне видалення у верхній таблиці спорожнює дві таблиці під нею.

У клубі каскад велить секретареві вирвати кожен рядок із номером члена, а потім кожен рядок у будь-якій іншій книзі, що вказував на вирваний рядок. Уявіть користувача, який писав дописи, а на ці дописи вказують коментарі інших людей. З каскадом на обох таблицях видалення цього одного користувача прибирає його дописи й коментарі всіх інших під ними.

Каскад іде за кожним правилом, що вказує на видалений рядок, а потім за кожним правилом, що вказує на ті. Остання книга втрачає рядки, які належали іншим людям.

Ніщо не скаже вам, що зникло разом із ним. Auth відповідає на успішне видалення порожньою відповіддю, панель показує на один запис менше, а рядки, що зникли в ту саму мить, просто бракуватиме наступного разу, коли їх хтось шукатиме.

Що зберігає кожна таблиця

Вирішуйте для кожної таблиці окремо, бо кожна тримає щось своє про людину, яка пішла.

Що тримає таблицяon deleteКоли користувача видалено
Лише саму людину: профіль, її налаштуванняcascadeРядки йдуть разом із нею
Те, що досі потрібне іншим: коментар у чужій гілці, запис про оплатуset nullРядки лишаються, колонка власника порожня
Усе, чим ви хочете спершу зайнятися власноручno action (за замовчуванням)Видалення відхиляють, поки ці рядки є

set null має дві умови. Колонка мусить приймати порожні значення, інакше видалення знову впаде з власним повідомленням, null value in column … violates not-null constraint. А політика виду user_id = auth.uid() більше ніколи нікому не покаже цей рядок, бо в SQL ніщо не дорівнює порожньому значенню. Рядок зберігається, а ваш застосунок перестає його будь-кому показувати.

Змінити правило означає прибрати його й додати знову з новою поведінкою, як і радить нотатка 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;

Візьміть назву правила, яку вивів запит вище, і свої таблицю та колонку.

Є ще один шлях, і він зберігає всі рядки. Supabase Auth уміє мʼяко видалити обліковий запис: auth.admin.deleteUser() приймає опцію shouldSoftDelete, і з нею Auth залишає рядок в auth.users, позначає його видаленим, перемішує email і телефон, стирає пароль і метадані та завершує сесії. Нічого не прибрано, тож жоден foreign key не спрацьовує. Це виконується на сервері з вашим секретним ключем, ніколи в браузері.

Те саме видалення за трьох правил. Перше відмовляє й нічого не змінює, друге забирає рядок, що вказує на члена, а третє лишає цей рядок із порожнім місцем.

Тригер, що не дістає до вашої таблиці

Якщо в рядку Postgres написано permission denied for table, то під час видалення спрацював тригер на auth.users і спробував торкнутися таблиці, куди йому не можна.

Роль, від імені якої працює Supabase Auth, supabase_auth_admin, має лише ті дозволи, яких потребує її власна робота. Нотатка Supabase пояснює це так: коли тригер, який виконує supabase_auth_admin, діє поза схемою auth, виникає помилка дозволів. Тригер, що прибирає після видаленого запису, скажімо пише у вашу власну таблицю журналу, падає саме так, якщо його функцію не створено з security definer.

Виправлення те саме, що й для реєстрації: функція з security definer, створена роллю postgres, яка робить одну вузьку річ. Версія цієї помилки для реєстрації розбирає її рядок за рядком, разом із порожнім search path, який робить її безпечною.

Якщо ваш застосунок використовує Prisma, спершу прочитайте третю причину в нотатці Supabase, бо вона називає Prisma одним зі способів зламати дозволи на auth.users.

Що стається з файлами, які людина завантажила?

У сучасному проєкті видалення проходить, а файли лишаються там, де були.

Посібник Supabase з керування користувачами досі попереджає, що не можна видалити користувача, якому належать обʼєкти в Supabase Storage. Це було правдою, поки storage.objects, таблиця, де Storage тримає рядок для кожного файла, мала foreign key під назвою objects_owner_fkey на auth.users. Storage відтоді прибрав цей ключ у власних міграціях схеми (0017-drop-owner-foreign-key.sql), і його посібник про власність файлів тепер каже, що власник записаний у колонці owner_id. Якщо запит вище показує storage.objects з objects_owner_fkey, у вашому проєкті ще стоїть старе правило, і попередження посібника стосується вас.

Інакше кожен файл, який людина завантажила, лишається у своєму бакеті після її відходу. У клубі пальта висять на гачках із жетоном, якого вже ні в кого немає. Політика виду owner_id = auth.uid()::text більше ніколи їх не знайде, а файл у публічному бакеті й далі віддається кожному, хто має посилання. Чи може стороння людина побачити список файлів у вашому бакеті, це одна з перевірок безкоштовного сканування Reeve.

Щоб прибрати їх, ідіть через сам Storage, зі сторінки Storage у панелі або через API Storage. Пряме видалення через SQL з його таблиць відхиляється, і Storage пояснює чому: "Direct deletion from storage tables is not allowed. Use the Storage API instead." Що копія бази тримає з цих файлів, а що лишає поза собою, описано в резервній копії Supabase Storage.

Зробіть копію, перш ніж міняти foreign key

У Postgres немає кошика, а каскад прибирає рядки з таблиць, на які ви навіть не дивилися.

Те, що змінює каскад чи set null, остаточне, щойно видалення вдалося, і єдиний шлях назад це копія, зроблена раніше. Безкоштовний план Supabase не тримає жодних бекапів. Pro тримає 7 днів щоденних копій, Team 14, а Enterprise до 30, і кожна з них живе в тому самому проєкті Supabase. Скопіювати базу самостійно можна кількома командами, а копія, що містить ваших користувачів, потребує третьої з трьох команд, які публікує Supabase.

Потім перевірте нові правила на тестовому записі, перш ніж на справжньому. Створіть запис, дайте йому по рядку в кожній таблиці зі списку запиту, видаліть його й подивіться, що лишилося в кожній таблиці.

Якщо вам спокійніше, коли ця копія вже існує до того, як хтось натисне видалити, саме для цього є бекапи Supabase від Reeve.

Шлях назад, коли видалення забрало більше, ніж ви хотіли

Каскад нічого не повідомляє про те, що прибрав. Ви дізнаєтеся, коли хтось спитає, куди поділися його коментарі.

Reeve Care тримає копію вашої бази Supabase з часу до видалення:

  • зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
  • кожна копія перевірена, перш ніж її зарахують, підрахунком рядків у кожній таблиці
  • відновлення в один клік, яке зберігає те, що є зараз, перш ніж замінити
  • ваші завантажені файли теж, щойно ви приєднаєте доступ до Storage
  • усе, що робить Reeve Monitor: усі девʼять перевірок вашого робочого застосунку щогодини й перевірка доступності кожні 60 секунд

Що зробити сьогодні

Що робити

  • Спробуйте видалити один раз, запишіть час і прочитайте рядок логу Postgres з тієї миті, перш ніж щось міняти.
  • Якщо він називає foreign key, виконайте запит вище, щоб побачити кожну таблицю, що вказує на ваших користувачів, і що кожна робить при видаленні.
  • Вирішуйте для кожної таблиці: cascade для рядків, що описують лише саму людину, set null для рядків, потрібних іншим, поведінка за замовчуванням для всього, що ви хочете прибрати власноруч.
  • Зробіть копію бази, перш ніж міняти будь-яке з цих правил, і спершу перевірте нові правила на тестовому записі.
  • Приберіть і файли людини через Storage. У сучасному проєкті обліковий запис іде, а файли лишаються.

Якщо рядки вже зникли далі, ніж ви хотіли, що можна повернути після видалення пояснює, де шукати й у якому порядку.

Поширені запитання

Що означає "Database error deleting user" у Supabase?

Це означає, що Supabase Auth спробував прибрати рядок людини з auth.users, щось у вашій базі відмовило, і все видалення скасовано. Обліковий запис такий самий, як був. Повідомлення не називає жодної таблиці, а рядок, який її називає, лежить у логах Postgres вашого проєкту в ту саму мить.

Чи варто додати on delete cascade, щоб це виправити?

На таблиці, яка описує лише саму людину, як таблиця профілів, так, і власний посібник Supabase робить саме це. На таблиці з тим, що досі потрібне іншим, як коментарі чи замовлення, спершу вирішіть. Cascade видаляє кожен рядок, що вказує на користувача, і кожен рядок, що вказує на ті, при кожному видаленні відтоді, і ніщо не повідомляє, що саме зникло.

Чи можна видалити користувача й зберегти його рядки?

Так, двома способами. Поставте foreign key на on delete set null, і рядки залишаться з порожньою колонкою власника, для чого ця колонка має приймати порожні значення. Або викличте auth.admin.deleteUser() із сервера з опцією shouldSoftDelete: Supabase Auth залишає рядок, позначає його видаленим, перемішує email і телефон та стирає пароль, тож нічого з того, що на нього вказує, не зачеплено.

Чому Supabase каже, що не можна видалити користувача, який завантажував файли?

Його посібник з керування користувачами досі так каже, і це було правдою, поки storage.objects мала foreign key під назвою objects_owner_fkey на auth.users. Supabase Storage відтоді прибрав цей ключ у власних міграціях схеми. У сучасному проєкті видалення проходить, а файли лишаються у своєму бакеті з owner_id, якого вже немає в жодного користувача.

Чи видалення користувача виводить його з системи?

Не одразу. За посібником Supabase, уже виданий токен доступу лишається дійсним, доки не спливе, навіть після видалення облікового запису. Видалення користувача прибирає його сесії, тож застосунок не отримає нового токена, але той, що вже на руках, працює до свого часу спливу.

Автор

Vlad Tkachenko

Засновник Reeve

Я щодня дивлюся на застосунки, зібрані в Lovable, Bolt, v0, Cursor і Replit, і на короткий список помилок, які трапляються в них знову і знову.

Більше про автора

Читати далі

Усі статті

Зробіть резервну копію бази Supabase перед наступною зміною

Reeve за розкладом зберігає зашифровану копію і відновить її за вас, якщо щось зламається.

Резервні копії Supabase

Автоматична зовнішня перевірка, а не повний аудит. Відсутність знахідок не є гарантією безпеки.