Skip to main content

Fix Supabase "Database error saving new user"

Why sign-ups fail with a 500 in Lovable, Bolt and Vite apps: profiles triggers, security definer, search_path and constraints

Updated 8 min readMarkdown

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.users that has an error
  • a constraint on auth.users that isn't being met
  • Prisma breaking the permissions on the auth.users table

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.

  1. Reproduce the failure once and note the time.
  2. 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."
  3. 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 is column "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 is not null but the function fills it from raw_user_meta_data, and this sign-up didn't send that field. Make the column nullable, add a default, or use coalesce in the function.
  • 23514 check_violation: a check constraint fails. The template in the users guide has username_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 example username, which is unique in 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 between auth.users and other tables and recreate them with a less restrictive constraint, as the guide suggests.
  • Prisma: Supabase lists Prisma breaking the permissions on auth.users as a known cause.
  • Users inserted with SQL: the related error Database error querying schema with converting NULL to string is unsupported (on a column such as confirmation_token) "typically happens when users are inserted directly via SQL or using an AI tool that uses a direct SQL INSERT, 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

  1. Run the trigger query again: is_security_definer should be true and settings should include the search_path.
  2. Sign up a brand-new test user from the live app, not the dashboard.
  3. Check that the user appears under Authentication > Users and that the matching public.profiles row exists.
  4. Sign that user out and back in, so any update trigger runs too.
  5. 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.

About the author

  • Founder and CEO of StacksMesh (stacksmesh.com). We fix, launch and migrate apps built with Base44, Lovable, Bolt and Replit, with a fixed price in writing before any work. I write about making AI-built apps safe and production-ready.

No comments yet. Be the first to comment.