Supabase responds with "user_not_found: User could not be found" when the identifier you supplied (email, phone, or OAuth provider) does not match any row inside auth.users. The fix is usually to confirm the user exists in the right project, create or restore the missing account, and call the correct sign-in flow so Supabase is looking for the provider you expect.
The Supabase Auth API looks up the user inside the `auth.users` table before it ever checks passwords or issues tokens. When that lookup returns nothing, the API immediately replies with "user_not_found: User could not be found" regardless of whether you sent a valid password. This can happen because: - The user never signed up or the row was deleted/disabled in auth.users - The identifier belongs to another Supabase project (anon key or URL mismatch) - You're calling a sign-in method for a different provider (password for an OAuth-only user) Because the user record is missing, Supabase cannot issue refresh/access tokens or run MFA, so the fix path focuses on the user storage and onboarding flow rather than the credentials themselves.
Use the service-role key on the server to inspect Supabase's internal table before you attempt to sign in:
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data: user, error } = await supabaseAdmin.auth.admin.getUserByEmail('[email protected]')
if (error) throw error
console.log('User row:', user)You can also query the row directly via SQL:
SELECT id, email, phone, provider, raw_user_meta_data
FROM auth.users
WHERE email = '[email protected]';This confirms whether the row exists, what provider it uses, and whether it belongs to the project tied to your service-role key.
If the user is genuinely missing, call signUp to insert them before signing in:
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'super-secure-password'
})
if (error) console.error('Sign-up error', error)For server-side workflows, create the account with the Management API and set email confirmation to true so you can seed accounts without sending an email:
await supabaseAdmin.auth.admin.createUser({
email: '[email protected]',
password: 'super-secure-password',
email_confirm: true
})Once the row exists, Supabase will stop throwing "user_not_found" for this identifier.
Check the provider stored on the user row before you call a login method:
const { data: user } = await supabaseAdmin.auth.admin.getUserByEmail(email)
if (user?.provider === 'github') {
await supabase.auth.signInWithOAuth({ provider: 'github' })
} else if (user?.provider === 'email') {
await supabase.auth.signInWithPassword({ email, password })
}If you call 'signInWithPassword' for an OAuth-only user, the Auth API is actually looking up the email for a password-enabled row and returns 'user_not_found' when it fails to find one. The same applies to OTP methods with phone numbers: use 'signInWithOtp({ email })' only when the row was created via email OTPs or magic links.
When the error reaches the UI, log the Supabase configuration and show a path forward:
if (error?.message.includes('user_not_found')) {
setAuthError(
'No account matches that identifier. Double-check your email, or tap Sign up to create a new account.'
)
}
console.log('SUPABASE_URL', process.env.NEXT_PUBLIC_SUPABASE_URL)
console.log('Anon key project', process.env.NEXT_PUBLIC_SUPABASE_URL?.split('.')[0])If your UI logs a different project ref than your service role key, you might be hitting a staging project that does not share the same users. Offering a clear error text helps users self-correct before the backend ever receives the request.
### How Supabase stores user identifiers
Supabase normalizes email addresses to lowercase before writing them to auth.users, so casing differences on the client side can cause you to look for an identifier that is stored differently. The user row also records the provider (email, phone, github, google, etc.), so the Auth API will only find a match if you call the corresponding sign-in method.
### Service role key is required for debugging
The anon key cannot list or inspect auth.users rows, so you must use the service-role key from Project Settings → API on a trusted server. Never expose the service-role key in front-end code.
### Project mismatch symptoms
If the error only occurs after deploying to production or a second environment, double-check that "SUPABASE_URL" and "NEXT_PUBLIC_SUPABASE_ANON_KEY" target the same project as the service-role key you used for diagnostics. Using an anon key from staging while diagnostics run against production will still show "user_not_found" because each project's auth.users table is separate.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists