← All posts
·7 min read·GuardLayer

Supabase "Database error querying schema": the fix

SupabaseAuthPostgresSecrets

"Database error querying schema" means Supabase Auth couldn't load the user row during sign-in, almost always because a column in auth.users is NULL where Auth expects a string. That happens when users were inserted with SQL instead of the Auth API. Fill the NULLs with '', then replace the seed SQL with auth.admin.createUser.

You call signInWithPassword, the request comes back as a 500, and the message is Database error querying schema. Nothing about it points at your schema, your tables or your migrations, which is why it's confusing. End users of plenty of apps see the same string, because it bubbles straight out of the Auth server.

What does "Database error querying schema" mean?

It means the Auth server tried to read a user from auth.users and the read failed for a reason other than "user not found". The message is hard-coded in Supabase Auth (GoTrue), in the password grant handler in internal/api/token.go:

user, err = models.FindUserByEmailAndAudience(db, params.Email, aud)
// ...
if err != nil {
    if models.IsNotFoundError(err) {
        return apierrors.NewBadRequestError(apierrors.ErrorCodeInvalidCredentials, InvalidLoginMessage)
    }
    return apierrors.NewInternalServerError("Database error querying schema").WithInternalError(err)
}

That branch tells you most of what you need. If the email doesn't exist, you get Invalid login credentials, a 400. If the row exists but can't be loaded into Auth's User struct, you get this 500. The word "schema" is a red herring: nothing is wrong with your public schema.

Depending on your supabase-js version, the client surfaces it as an AuthApiError with status 500 (which is why people search "authapierror database error querying schema") or, in recent auth-js releases that treat 5xx as transient, as an AuthRetryableFetchError that keeps the same message. Retrying won't help: the row stays broken until you fix it.

Why a NULL breaks sign-in

Auth loads users into a Go struct. Several text columns in that struct are typed as plain string, not a nullable type. In internal/models/user.go those are:

  • confirmation_token
  • recovery_token
  • email_change_token_current
  • email_change_token_new
  • email_change
  • phone_change_token
  • phone_change
  • reauthentication_token
  • aud and role (almost always set, but also plain strings)

email and phone are nullable, so those are fine. A Go string can't hold SQL NULL, so when any of the columns above is NULL, the scan fails with converting NULL to string is unsupported. Auth doesn't treat that as "not found", so it returns the generic 500. Supabase's own troubleshooting article says the same thing: NULLs where Auth expects a string, usually after manual SQL inserts or updates to auth.users. A recent report, supabase/supabase#51545, shows exactly that scan error on confirmation_token.

Users created through signup, invites or the Admin API never hit this, because Auth writes those columns itself. Users created with a hand-written INSERT only get the columns the script listed, and anything without a default ends up NULL.

Where these rows come from

Usually it's a seed script. Ask an AI assistant for "seed an admin user in Supabase" and you'll often get something like this:

-- supabase/seed.sql
insert into auth.users (
  instance_id, id, aud, role, email,
  encrypted_password, email_confirmed_at, created_at, updated_at
) values (
  '00000000-0000-0000-0000-000000000000',
  gen_random_uuid(), 'authenticated', 'authenticated',
  'admin@example.com',
  crypt('SuperSecret123!', gen_salt('bf')),
  now(), now(), now()
);

It runs without errors, and the user shows up in the dashboard. Then sign-in fails, because none of the token columns were listed. The CLI applies seed.sql on local resets; the trouble usually starts when the same file gets pasted into the SQL Editor of a hosted project.

The NULLs are the visible bug. The script has two quieter ones.

It commits a password to git. crypt('SuperSecret123!', ...) puts the plaintext in the repo, and the hash goes into the database. If that's the real admin password on a hosted project, anyone with read access to the repo, now or later, can log in as admin. Deleting the line in the next commit doesn't remove it from history.

It skips everything Auth enforces. A raw insert bypasses the password policy, including leaked password protection, and it doesn't create the matching auth.identities row that the Auth API writes for an email user. You end up with an account Auth didn't create and may not handle the way it handles every other user.

How do I fix "Database error querying schema"?

Find which columns are NULL, set them to an empty string, and stop creating users with SQL. First, look before you change anything:

select
  count(*) filter (where confirmation_token is null)          as confirmation_token,
  count(*) filter (where recovery_token is null)              as recovery_token,
  count(*) filter (where email_change_token_current is null)  as email_change_token_current,
  count(*) filter (where email_change_token_new is null)      as email_change_token_new,
  count(*) filter (where email_change is null)                as email_change,
  count(*) filter (where phone_change_token is null)          as phone_change_token,
  count(*) filter (where phone_change is null)                as phone_change,
  count(*) filter (where reauthentication_token is null)      as reauthentication_token
from auth.users;

Any non-zero count is a candidate. The Auth logs in the dashboard may show the underlying scan error with the exact column name, which confirms it.

Supabase's documented repair is one column at a time:

update auth.users
set confirmation_token = ''
where confirmation_token is null;

Repeat for each column that had NULLs. Scope it with where id = '...' if you know which user is broken, rather than touching the whole table.

On a local stack or a throwaway dev project, that's the end of it. On production, be more careful. auth is Supabase's schema, not yours, and a bulk update to it is hard to undo. Run the select first, change only the rows and columns it flags, and take a backup. If the counts don't match what you expect, or you never seeded users by hand and still see this error, stop and open a ticket with Supabase support instead of experimenting on live auth data.

Seed users the supported way

Once sign-in works, delete the insert into auth.users from your seed file and create users through the Admin API from a server-only script:

// scripts/create-admin.ts (run locally or in CI, never imported by app code)
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // or your sb_secret_ key
  { auth: { persistSession: false, autoRefreshToken: false } }
);

const { data, error } = await supabase.auth.admin.createUser({
  email: process.env.SEED_ADMIN_EMAIL!,
  password: process.env.SEED_ADMIN_PASSWORD!,
  email_confirm: true,
});

if (error) throw error;
console.log("Created", data.user.id);

Auth writes every column itself, creates the identity row, and runs its configured password checks on the password you pass. The credentials live in env vars, so nothing sensitive lands in the repo. Keep the admin key out of anything the browser loads.

If the old seed ever ran against a hosted project with a real password, change that user's password now and treat the committed one as public.

If you're also seeing errors at signup rather than sign-in, that's a different failure with a different fix: Database error saving new user is almost always a trigger on auth.users throwing.

Quick self-check

# Any SQL that writes to auth.users?
grep -rniE "insert into auth\.users|update auth\.users" --include=*.sql .

# Any literal passwords hashed in SQL?
grep -rnE "crypt\('[^']+'" --include=*.sql .

A hit on the first is where the NULLs come from. A hit on the second is a password in your git history. GuardLayer doesn't flag seed SQL today; its free read-only Live check looks at Auth settings such as leaked password protection and email confirmation, plus RLS and policies, and never reads users or rows.

FAQ

Is "Database error querying schema" a problem with my database schema? No. It's a generic message from the Auth server when it can't load a user row. The usual cause is a NULL in a text column of auth.users, not anything in your own tables.

Why does sign-in fail for one user but work for others? Only rows with NULLs in the affected columns fail. Users created through signup or the Admin API have those columns filled; users inserted with SQL often don't.

Is it safe to run the UPDATE on production? On rows you've confirmed have NULLs, setting them to '' is Supabase's documented fix. Check with a select first, scope the update narrowly, back up, and contact Supabase support if anything doesn't match what you expect.

How should I seed test users instead? Call supabase.auth.admin.createUser from a server-side script with the email and password read from env vars. Auth fills every column and applies your password policy.

Check your Supabase Auth settings and tables, free

Connect your Supabase project read-only. GuardLayer checks your Auth settings (leaked passwords, email confirmation, secure email change, token rotation, CAPTCHA, MFA) and your RLS, policies and grants, and says how to fix each problem. It never reads your rows and can't change anything.

Check my Supabase project

Prefer to check code instead? Scan your repo, no signup.

Keep reading