# Fix Supabase "Database error saving new user"

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](https://supabase.com/kb/troubleshooting/database-error-saving-new-user-RU_EwB) and [Errors when creating / updating / deleting users](https://supabase.com/kb/troubleshooting/dashboard-errors-when-managing-users-N1ls4A) troubleshooting guides, the [Managing user data](https://supabase.com/docs/guides/auth/managing-user-data) guide, the [Database functions](https://supabase.com/docs/guides/database/functions) guide and the [Auth error codes](https://supabase.com/docs/guides/auth/debugging/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](https://supabase.com/docs/guides/telemetry/logs) 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](https://www.postgresql.org/docs/current/errcodes-appendix.html)) 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`](https://www.postgresql.org/docs/current/catalog-pg-trigger.html) and [`pg_proc`](https://www.postgresql.org/docs/current/catalog-pg-proc.html) catalogs:

```sql
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 `GRANT`s for `authenticated` don't help. In [supabase-js issue #1514](https://github.com/supabase/supabase-js/issues/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:

```sql
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`](https://www.postgresql.org/docs/current/sql-createfunction.html) changes the function without breaking the trigger that uses it. If you have no trigger yet, add it:

```sql
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](https://stacksmesh.com/fix/supabase-new-row-violates-row-level-security-policy?utm_source=hashnode&utm_medium=article&utm_campaign=PO-0101). And if you're about to launch, our free [Launch Check](https://stacksmesh.com/tools/launch-check?utm_source=hashnode&utm_medium=article&utm_campaign=PO-0101) looks at headers, exposed keys and more, with no sign-up.

I wrote this with AI assistance and reviewed it myself.

