Основи безпеки
"Database error saving new user" у Supabase: де шукати
"Database error saving new user" означає, що щось причеплене до вашої реєстрації в Supabase впало. Справжня помилка лежить у ваших логах.

Коротко
- "Database error saving new user" означає, що Supabase прийняв реєстрацію, щось причеплене до неї впало, і все це скасували. Жодного облікового запису не створено.
- Повідомлення навмисно нічого не каже, а справжнє вже десь записане. Власна сторінка з розвʼязанням у Supabase веде спершу в логи Auth, а тоді в логи Postgres по рядок, що лежить під ними.
- Майже завжди це тригер на auth.users, який не дістає до таблиці, куди пише. Відповідь це функція security definer, і влучити треба в те, скільки їй дозволено.
Хтось намагається створити обліковий запис у вашому застосунку й отримує у відповідь щось, що взагалі не про нього:
Database error saving new user
Ніхто не може зареєструватися. Форма працювала, коли ви її будували, ваш проєкт піднятий, і єдине, що у вас є, це речення без жодної таблиці, без стовпця і без причини.
Ось те, що гайд за гайдом розуміє неправильно: це повідомлення зробили так, щоб воно вам нічого не казало, а ваша справжня помилка вже записана в іншому місці. Пошукайте цей рядок, і вам простягнуть блок SQL, щоб вставити, зазвичай чиюсь версію функції, яка пише в таблицю з чужими назвами стовпців. Вставити це або спрацює, або додасть другу проблему до першої. Рядок, який каже, що саме впало, лежить у логах вашого власного проєкту, і він називає таблицю.
Що означає "Database error saving new user"
Supabase прийняв новий обліковий запис і потім скасував його, бо щось причеплене до реєстрації впало.
Реєстрація пише один рядок, у таблицю auth.users, якою Supabase керує за вас.
Більшість проєктів чіпляють до цього запису ще щось: тригер, який копіює нову
людину у вашу власну таблицю profiles, обмеження, зовнішній ключ. Коли ця
причеплена річ видає помилку, база скасовує всю операцію, а це включає й рядок у
auth.users. Жодного напівзробленого запису не лишається. І у відповіді теж
немає нічого, що сказало б, яка саме з причеплених речей це була.
Уявіть стійку з підсобкою позаду. Хтось розписується в журналі на стійці, а працівник несе нове імʼя назад, щоб переписати його в другу книгу. Якщо працівник не може зайти в кімнату, де та книга лежить, запис не рахується: рядок на стійці перекреслюють, а людині кажуть, що сталася проблема. Чого їй ніхто не каже, так це того, що проблемою були замкнені двері за дванадцять метрів.
Невиразність тут навмисна, і це правильне рішення. Ваша форма реєстрації відкрита в інтернет. Повідомлення, яке називало б ваші таблиці, описувало б вашу базу кожному, хто готовий ввести туди адресу пошти.
Якщо натомість у відповіді email rate limit exceeded, ваша база тут ні до
чого: Supabase відмовився надсилати лист підтвердження, і
ця помилка має окрему сторінку.
Де лежить справжня помилка?
У логах вашого проєкту, у два кроки, і власна сторінка з розвʼязанням у Supabase задає порядок.
Відтворіть її один раз, перш ніж шукати, щоб мати час, за яким звіряти. Спробуйте зареєструватися у вашому живому застосунку, дайте цьому впасти й запишіть хвилину.
Далі, у панелі Supabase:
- Відкрийте Logs і прочитайте логи Auth. Знайдіть запис за той час. Він несе помилку, яку Auth записав від себе, і вона конкретніша за речення, що дісталося вашому застосунку.
- Якщо цей запис показує на базу, поверніться в Logs і відкрийте логи Postgres. Рядок за ту саму хвилину це ваша справжня помилка, написана звичайним словником Postgres: стовпець, відношення, тип, дозвіл.
Нотатка Supabase з розвʼязанням для цього повідомлення перелічує три причини. У
вас є тригер або функція тригера на таблиці auth.users, у якій є помилка. Ви
додали на auth.users обмеження, яке не виконується. Або ви користуєтеся Prisma,
і вона зламала дозволи на auth.users. З тієї ж нотатки походить і дворівневий
шлях логами вище.
Для застосунку, зробленого в Lovable, Bolt, Replit чи v0, перша причина покриває
більшість випадків, бо копіювати нову реєстрацію в таблицю profiles це взірець,
до якого тягнеться кожен білдер, а тригер це те, як воно робиться.
Причина 1: тригер не дістає до вашої таблиці
Роль бази, яка виконує ваш тригер, не має права виходити зі схеми auth, а ваш
тригер пише саме назовні.
Supabase пояснює це на власній дошці обговорень, і формулювання варто мати
точним. Роль бази, яка керує автентифікацією, supabase_auth_admin, має лише ті
дозволи, що потрібні їй для власних обовʼязків, тож коли тригер, яким керує
supabase_auth_admin, взаємодіє поза схемою auth, це спричиняє помилку дозволу.
Це перепустка працівника. Вона відчиняє передній офіс, бо саме там його робота. Друга книга лежить у кімнаті, на яку цю перепустку ніколи не виписували, а замкнені двері нічого не повідомляють про те, чому вони замкнені.
У лозі Postgres це приходить як помилка дозволу, що називає таблицю, у яку ваш тригер писав.
Відповідь, і що вона віддає
security definer на функції, яка змушує її працювати з правами того, хто її
створив, а не того, хто її викликав.
Supabase каже це одним рядком: функція security definer зберігає права
користувача бази, який її створив, і доки це роль postgres, ваші тригери auth
мають давати раду таблицям назовні. Працівник перестає носити власну перепустку й
носить перепустку керівника.
Це справжня передача повноважень, тож варто знати, що ви там написали, перш ніж
вставляти. Кожна інструкція всередині цієї функції тепер виконується від імені
власника. Власний приклад Supabase ставить порожній search_path, через
security definer set search_path = '', і саме через це вставка там називає
public.profiles повністю, а не просто profiles. З порожнім search_path гола
назва таблиці більше не розвʼязується ні в що. Функції, яка несе права власника,
після цього ніхто не підмінить тихцем ціль на іншу таблицю залежно від того, що
саме каже search_path того, хто викликає.
Чого це не дозволяє, так це тягнутися по security definer наступного разу, коли
щось відмовлять. Лишіть функцію робити одну річ і повертатися, як це робить
версія Supabase.
Причина 2: правило на таблиці, у яку ви пишете
Row Level Security на вашій таблиці profiles може відхилити вставку, і тоді
реєстрація падає точно так само.
Посібник Supabase до цього взірця каже ввімкнути Row Level Security для тієї таблиці, і ця вказівка правильна. Для вашого тригера це означає, що вставка має бути дозволена: або policy, яка її покриває, або функцією, що працює від імені того, на кого policy не поширюються.
Ось тут це перестає бути багом і стає рішенням, яке хтось ухвалює об
одинадцятій вечора. Ніхто не може зареєструватися, помилка нічого не каже, а в
панелі є перемикач, підписаний звичайними словами, від якого помилка зникає:
вимкнути Row Level Security на profiles. Наступна спроба спрацьовує.
Відбувається й інше: таблиця з вашими користувачами, з усім, що ви тримаєте там
поруч із їхніми іменами, починає відповідати кожному, хто попросить.
Цю частину видно ззовні, і саме так ми можемо її порахувати. Наш сканер читає живі застосунки так, як це робив би незнайомець. Ми знайшли проєкт Supabase за 8 435 застосунками з тих, яким він виставив оцінку. На 3 680 із них питання "чи може хтось без облікового запису прочитати цю таблицю" отримало придатну відповідь, і 2 096 відповіли так. У 394 з них відкрита таблиця мала назву таблиці з людьми.
Частина цього навмисна, бо опубліковане меню й сторінка оголошень лежать у таблицях, які незнайомець і має читати, а наш сканер не відрізнить їх від таблиці клієнтів, яку хтось відкрив, щоб здихатися помилки. Він повідомляє, що саме відповіло. Якщо ви радше побачите власну відповідь, ніж будете про неї міркувати, наше безкоштовне сканування читає ваш живий застосунок і каже, які таблиці відповіли. Це триває близько 20 секунд і не потребує облікового запису: просканувати застосунок.
Policy, якої потребує тригер, значно вужча за вимикання правил. Увімкнути Row Level Security на кожній таблиці показує, як її написати, а "new row violates row-level security policy" це повідомлення, яке ви отримуєте, коли policy відхиляє вставку, що ви бачите власними очима.
Інші причини, і як кожна читається в лозі
Ще чотири, і рядок у лозі розрізняє їх кількома словами.
Таблиця, якої немає. Нотатка Supabase покриває й це: функція тригера, яка
показує на таблицю, що не існує або була видалена. Таблиця profiles, видалена й
відбудована під іншою назвою, забирає з собою кожну реєстрацію, а лог називає
відношення, якого бракує.
Обмеження на auth.users. Перевірка або зовнішній ключ, доданий на ту
таблицю, якого новий рядок не задовольняє. Це варто шукати, навіть коли ви
впевнені, що не додавали тригера, бо зовнішній ключ між вашою власною таблицею та
користувачем auth це найпоширеніший шлях, яким ця причина приходить. З
поведінкою за замовчуванням той самий зовнішній ключ потім відмовляє й у
видаленні цієї людини, і
це повідомлення має власну статтю.
Значення не того типу. Ця зʼявляється слово в слово. Тригер, який копіює поля
реєстрації з raw_user_meta_data, читає їх через ->>, а ->> повертає текст,
хоч би як значення виглядало на вході. Записати цей текст у стовпець date дало,
в одному описаному випадку,
column "birth_date" is of type date but expression is of type text. Полагодити
це можна приведенням типу, спершу взявши витягування в дужки:
(new.raw_user_meta_data->>'birthDate')::date,
(new.raw_user_meta_data->>'height')::integer,
Рядки, вставлені руками. Нотатка Supabase покриває й облікові записи,
створені напряму через SQL замість API Auth, де обовʼязкові стовпці схеми auth
лишаються NULL замість порожніх рядків. Якщо хтось засіяв користувачів через
insert, це інше полагодження, ніж усі вище.
Ще один рядок, який можна там зустріти:
duplicate key value violates unique constraint. Він означає, що рядок, який
пише ваш тригер, там уже був, наприклад тому, що ваш застосунок вставляє той самий
профіль після реєстрації, і
про цю помилку є окремий допис.
І ще одна: null value in column … violates not-null constraint. Ваш тригер
записав порожнє значення в стовпець, який його вимагає, найчастіше це поле
реєстрації, яке форма так і не надіслала, і
про неї теж є окремий допис.
Остання, яку ви можете зустріти, це
insert or update on table "profiles" violates foreign key constraint. Зазвичай
вона означає, що тригер створили з before insert, тож людину, для якої він
записує профіль, ще не збережено. Шаблон Supabase нижче використовує
after insert, і
про це повідомлення є окремий допис.
Робоча версія, повністю
Починайте зі взірця, який публікує Supabase, а не з результату пошуку, бо чотири речі в ньому несуть вагу, і переказ зазвичай одну з них губить.
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
insert into public.profiles (id, first_name, last_name)
values (new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name');
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
Ці чотири: security definer, порожній search_path, повністю написане
public.profiles і return new, яке дає реєстрації йти далі. Ваші власні назви
стовпців ідуть у вставку, і кожен стовпець, який ви там назвете, має існувати й
має приймати те, що дає ->>.
Посібник Supabase додає речення, яке варто повторити: якщо тригер впаде, він може заблокувати реєстрації, тож перевіряйте свій код ретельно. Створюйте справжній обліковий запис у живому застосунку після кожної зміни в ньому.
Як тримати реєстрацію робочою після змін
Реєстрація це та частина застосунку, яку ви перестаєте перевіряти того дня, коли вона запрацювала. Ви зареєструвалися один раз, у вас є обліковий запис, а далі ви просто входите. Наступний промпт, що торкнеться ваших таблиць, може зламати реєстрацію для всіх інших, доки ваша власна сесія поводиться цілком нормально.
Reeve Monitor дивиться за вас:
- усі девʼять перевірок щогодини, для трьох застосунків
- лист того дня, коли нове сканування оцінить ваш застосунок гірше за попереднє
- чи застосунок доступний, кожні 60 секунд
- щомісячний звіт про те, що він бачив
Правило, вимкнене, щоб здихатися помилки, лишає таблицю з людьми читабельною. Reeve Care тримає копію вашої бази Supabase там, куди жодна зміна ваших правил не дістане.
- зашифрована копія щодня, збережена поза вашим обліковим записом Supabase
- кожна копія перевірена, перш ніж її зарахують, підрахунком рядків у кожній таблиці
- відновлення в один клік, яке зберігає те, що є зараз, перш ніж замінити
- ваші завантажені файли теж, щойно ви приєднаєте доступ до Storage
- усе, що робить Monitor
Що зробити сьогодні
Що робити
- Відтворіть одну невдалу реєстрацію й запишіть час, перш ніж щось міняти.
- Прочитайте запис логу Auth за ту хвилину, а тоді рядок логу Postgres за той самий час, якщо запис Auth показує на базу. Той рядок називає стовпець, відношення або дозвіл, на якому все стало.
- Перевірте, що ваша функція тригера несе
security definerі називає ціль повністю, якpublic.profiles, а неprofiles. - Якщо policy на тій таблиці відхиляє вставку, напишіть policy для неї. Вимкнути Row Level Security прибирає помилку й лишає ту таблицю читабельною для кожного, хто попросить.
- Створюйте справжній обліковий запис у живому застосунку після кожної зміни в тригері реєстрації. Ваша власна сесія повністю ховає від вас зламану реєстрацію.
Почніть із таблиці, у яку пише ваш тригер, а тоді прочитайте policy на кожній таблиці, якої ви торкалися того самого тижня. Десятихвилинний перелік із безпеки покриває те, що ще лишається відкритим у щойно запущеному застосунку, а посібник із безпеки Supabase проходить решту того, до чого може дістатися незнайомець.
Поширені запитання
Що означає "Database error saving new user"?
Означає, що Supabase записав нову людину у власну таблицю auth.users, щось причеплене до цього запису впало, і база скасувала всю операцію разом із новим рядком. Тобто жодного облікового запису не створено, і нічого недоробленого прибирати не треба. Повідомлення навмисно не називає ні таблиці, ні причини: ваша форма реєстрації відкрита в інтернет, і конкретна помилка описувала б вашу базу кожному, хто введе туди адресу пошти.
Де знайти справжню помилку?
У логах вашого проєкту, і власна сторінка з розвʼязанням у Supabase задає порядок. Спробуйте зареєструватися, дайте цьому впасти й запишіть час. Відкрийте Logs і прочитайте запис Auth за ту хвилину: він несе те, що Auth записав від себе. Якщо цей запис показує на базу, відкрийте логи Postgres за той самий час: той рядок називає стовпець, відношення або дозвіл, на якому все справді стало.
Чи ламає Row Level Security на моїй таблиці profiles реєстрацію?
Може ламати. Якщо ваш тригер вставляє в таблицю з Row Level Security і жодна policy цієї вставки не дозволяє, вставку відхиляють, і реєстрація падає з тим самим невиразним повідомленням. Відповідь це policy для неї або функція, яка працює з правами, на які policy не поширюються. Вимкнути правила на тій таблиці теж прибирає помилку, і лишає таблицю з вашими користувачами читабельною для кожного, хто попросить.
Що таке security definer і чи це безпечно?
Це змушує функцію працювати з правами того, хто її створив, а не того, хто її викликав, і саме це дозволяє тригеру, який виконує роль auth, писати у вашу власну таблицю. Це взірець, який публікує Supabase, і це справжня передача повноважень, тож функція має робити одну вузьку річ. Supabase до того ж ставить порожній search_path у своєму прикладі, і через це вставка там називає public.profiles повністю, а не просто profiles.
Я ніколи не створював тригера. Що це ще може бути?
Ще три речі. Обмеження або зовнішній ключ, доданий на auth.users, якого новий рядок не задовольняє. Функція тригера, яка показує на таблицю, перейменовану чи видалену. І нотатка Supabase називає Prisma поіменно, бо вона може зламати дозволи на auth.users. Яке з них у вас, скаже рядок у лозі Postgres.