A new user fills in your sign-up form, clicks the button, and gets nothing. Your console shows a 500 from Supabase Auth with the message Database error saving new user. Existing users can still sign in. Nobody new can get in.
I see this a lot in Lovable, Bolt and Vite apps that keep a profiles table next to Supabase Auth. It sounds like Supabase is broken. It almost never is: something in your own database stopped the user row from being written.
Below is how I track it down, in order. Everything here comes from Supabase's Database error saving new user and Errors when creating / updating / deleting users troubleshooting guides, the Managing user data guide, the Database functions guide and the Auth error codes page, all read on 10 November 2026.
What the error actually means
When someone signs up, Supabase Auth inserts a row into auth.users. If anything attached to that insert fails, the transaction is aborted and Auth returns a generic wrapper. The users troubleshooting guide lists the family (Database error saving new user, Database error creating new user, Database error updating user and others), returned as HTTP 500 with the code unexpected_failure.
The error codes page says the same thing in one line: a 500 from Auth "most often" points to "issues in your database setup such as a misbehaving trigger on a schema, function, view or other database object."
Supabase lists three common causes:
- a trigger or trigger function on
auth.usersthat has an error - a constraint on
auth.usersthat isn't being met - Prisma breaking the permissions on the
auth.userstable
In the apps we check, it is usually the first one: a trigger that copies each new user into public.profiles.
Step 1: read the real error, not the wrapper
The message your app gets is the wrapper. The real error is in the logs.
- Reproduce the failure once and note the time.
- Open Logs in the dashboard and select the Auth log type. Supabase says the Auth logs "surface the specific error message and give you the most direct signal about what went wrong."
- If that entry points at the database, switch to Postgres logs at the same time. The Logs guide notes you can filter Postgres events by SQLSTATE.
The Postgres entry usually names the function and line, for example:
event_message: "relation "profiles" does not exist"
context: "PL/pgSQL function public.handle_new_user() line 3 at SQL statement"
That is the line you fix. The SQLSTATE code (Postgres error codes) tells you which section below applies.
Step 2: see which triggers run on auth.users
Run this in the SQL editor. It uses the standard pg_trigger and pg_proc catalogs:
select t.tgname as trigger_name,
p.proname as function_name,
p.prosecdef as is_security_definer,
p.proconfig as settings
from pg_trigger t
join pg_proc p on p.oid = t.tgfoid
where t.tgrelid = 'auth.users'::regclass
and not t.tgisinternal;
If is_security_definer is false for a function that writes to public, you have probably found the cause.
Cause 1: permission denied for table profiles (42501)
The log looks something like permission denied for table profiles (SQLSTATE 42501).
The users troubleshooting guide explains why: the role that manages authentication, supabase_auth_admin, "only has the necessary permissions it needs to perform its duties." When a trigger it fires touches a table outside the auth schema, that is a permission error.
That is why RLS policies or GRANTs for authenticated don't help. In supabase-js issue #1514, a Supabase maintainer explains that Auth connects as supabase_auth_admin and plpgsql trigger functions run as security invoker by default, so the trigger has no rights on profiles whatever you grant to authenticated.
The fix is to recreate the function as security definer. The guide says such a function "retains the privileges of the database user that created it", and as long as that is postgres, the trigger can reach your tables. This is the pattern from Managing user data:
create or replace 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 or replace changes the function without breaking the trigger that uses it. If you have no trigger yet, add it:
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
Cause 2: relation "profiles" does not exist (42P01)
Two ways to get here.
The table really is missing. The troubleshooting guide calls this common "when a profiles table is referenced in a trigger function but was never created, or was accidentally dropped." Create the table, or remove the reference from the function on the Database Functions page.
Or the table exists but the function can't find it. The Database functions guide says that when you use security definer, you must set the search_path, and with search_path = '' you must name the schema for every relation, such as public.profiles. A function that says insert into profiles with an empty search path fails with 42P01. The users guide lists this as "Broken search path / incorrect name".
Cause 3: the trigger and the table disagree
The insert reaches the table and the table rejects it. The Postgres logs name the problem:
-
42703
undefined_column: the function writes a column the table doesn't have. The users guide example iscolumn "updated_at" of relation "profiles" does not exist. Someone renamed or dropped a column, and the function was never updated. -
23502
not_null_violation: a column isnot nullbut the function fills it fromraw_user_meta_data, and this sign-up didn't send that field. Make the column nullable, add a default, or usecoalescein the function. -
23514
check_violation: a check constraint fails. The template in the users guide hasusername_length check (char_length(username) >= 3), so a two-letter username copied from metadata blocks the whole sign-up. -
23505
unique_violation: a unique column already has that value, for exampleusername, which isuniquein the same template. Let the app set that column after sign-up instead of copying it from metadata.
Managing user data warns: "If the trigger fails, it could block signups, so test your code thoroughly." Keep the trigger small.
Cause 4: something else on auth.users
If no trigger is involved:
-
A constraint on
auth.users: check key relationships betweenauth.usersand other tables and recreate them with a less restrictive constraint, as the guide suggests. -
Prisma: Supabase lists Prisma breaking the permissions on
auth.usersas a known cause. -
Users inserted with SQL: the related error
Database error querying schemawithconverting NULL to string is unsupported(on a column such asconfirmation_token) "typically happens when users are inserted directly via SQL or using an AI tool that uses a direct SQLINSERT, rather than through the Auth API." Their fix sets that column to an empty string where it is null. Create users through the Auth API.
When sign-in breaks too
If existing users also get a 500 on sign-in, look for a trigger on update of auth.users. The same maintainer explains that error update user's last_sign_in field wraps the update Auth runs when a session is created or refreshed. The fix is the same: security definer with a pinned search path.
Keep the security definer function narrow
The Database functions guide is blunt about the risk: a function created in the dashboard or by a migration is owned by postgres, "which bypasses Row Level Security", and the guide says to prefer security invoker by default. So keep the trigger function to one job: insert the new user's own row using new.id.
How to prove it's fixed
- Run the trigger query again:
is_security_definershould betrueandsettingsshould include thesearch_path. - Sign up a brand-new test user from the live app, not the dashboard.
- Check that the user appears under Authentication > Users and that the matching
public.profilesrow exists. - Sign that user out and back in, so any update trigger runs too.
- Re-check Auth and Postgres logs for that time.
In app code, branch on error.code, not the message text, as the error codes page recommends.
Most cases I see come down to a profiles trigger written as security invoker, or a table that changed after the trigger was written. Once the real Postgres error is in front of you, the fix is usually a few lines of SQL.
If the error you see is new row violates row-level security policy when your app writes rows, that is a different problem, and I wrote up that fix here: Fix "new row violates row-level security policy" in Supabase. And if you're about to launch, our free Launch Check looks at headers, exposed keys and more, with no sign-up.
I wrote this with AI assistance and reviewed it myself.
Top comments (0)