Replace Supabase Auth Without Moving Your Database
Identity · practitioner · 9 min read · last reviewed 2026-09-13
Supabase supports external JWT issuers, so you can swap auth providers and keep Postgres, RLS, Storage, and Realtime untouched. The four hard requirements, the bcrypt export, and Auth0 versus MojoAuth.
TL;DR
- Supabase officially supports Clerk, Firebase Auth, Auth0, AWS Cognito, and WorkOS as third-party JWT issuers, so replacing auth rarely requires a database migration.
- The provider must sign asymmetrically, expose an OIDC discovery URL, set a kid header, and add a role claim of 'authenticated', or PostgREST has no role to switch into and policies fail closed.
- Supabase stores standard bcrypt hashes in auth.users.encrypted_password, so passwords port to any bcrypt-verifying system without a reset.
- A full exit breaks four things: foreign keys to auth.users(id), RLS policies calling auth.uid(), the anon and authenticated Postgres roles, and Storage plus Realtime.
- Auth0 and MojoAuth both offer 25,000 free monthly active users; only Auth0 is on Supabase's supported issuer list, and neither includes MFA on the free plan.
Replacing Supabase Auth usually does not require moving your database. Supabase supports external identity providers as first-class JWT issuers, so you can keep Postgres, your row level security policies, Storage, and Realtime exactly as they are, and change only who signs the token. Auth0 is on the supported list. MojoAuth is not, which is the main thing separating them for this job. This guide covers both the keep-the-database path and the full exit, including what to do with your password hashes.
Decide whether you are moving the database at all
Most people asking this question want one of three things: multi-factor authentication Supabase does not give them on their plan, enterprise SSO for a B2B deal, or a passwordless login flow. None of those require a database migration.
Supabase officially supports third-party auth with Clerk, Firebase Auth, Auth0, AWS Cognito, and WorkOS. In that setup the external provider issues the session token, Supabase verifies it against the provider's public keys, and every policy you have already written keeps working. There is no user export, no hash migration, and no translation layer.
You are looking at a real migration only if you are leaving Supabase Postgres too, or if your chosen provider is not on that list. If you are moving the database, read Migrate Supabase Postgres to Neon alongside this, because the two migrations have different failure modes and should not be attempted in the same change.
How Supabase actually reads identity
You cannot plan this migration without knowing where auth.uid() gets its answer, because that one function is load-bearing in almost every policy you have written.
auth.uid() and auth.jwt() are Supabase-defined SQL functions in the auth schema. They are not core Postgres. When a request arrives, PostgREST validates the JWT, sets the claims into a request-scoped setting, and issues a SET LOCAL ROLE using the token's role claim. That is how a request ends up running as anon or authenticated. auth.uid() returns the sub claim. auth.jwt() returns the whole claims object.
Three details cause real bugs:
- `auth.uid()` returns null for an unauthenticated request. A policy written as
auth.uid() = user_idbehaves differently from one written asauth.uid() IS NOT NULL AND auth.uid() = user_id. Audit for the first form before you change anything. - `raw_user_meta_data` is writable by the user. It shows up in
auth.jwt()and must never appear in a policy.raw_app_meta_datais the immutable one. - A JWT is stale until it refreshes. Revoking a role does not take effect until the next refresh, so plan for the lag rather than assuming instant propagation.
Path A: swap the issuer, keep everything else
This is the path to take unless you have a specific reason not to. Four requirements, and all four are hard requirements rather than recommendations.
- Asymmetric signing. The provider must sign with a private key and publish the public key. Symmetrically signed tokens are not supported, so a shared-secret setup will not work.
- An OIDC discovery URL. Supabase reads the provider's JWKS from it to find verification keys.
- A `kid` header on every token. Without it Supabase cannot pick the right key from the key set.
- A `role` claim with the value `authenticated`. This is the step teams miss. PostgREST uses that claim for its
SET LOCAL ROLE. Without it there is no role to switch into and your policies do not behave, usually by silently denying everything.
With Auth0 that fourth requirement is a custom claim added through a Login Action. Key rotation propagates in up to roughly thirty minutes, so do not rotate signing keys during the cutover window.
What does not change: your tables, your policies, your Storage buckets, your Realtime subscriptions, and your anon and authenticated roles. What does change is who owns the user record. Supabase stops writing to auth.users, so you need your own public.users table keyed on the external provider's subject, and anything holding a foreign key to auth.users(id) has to point at it instead.
That foreign key is the work. A profiles.id references auth.users(id) pattern is close to universal in Supabase projects, and repointing it is a migration you should write, test on a branch, and run once.
Path B: the full exit
If you are leaving Supabase entirely, four things break, roughly in order of how much they will cost you.
| What | What happens | What you do |
|---|---|---|
FKs to auth.users(id) | Inserts fail once the schema stops being populated | Own a public.users table, repoint every key |
| RLS policies | auth.uid() has no source | Move authorization into the application layer |
anon / authenticated / service_role | Do not exist outside Supabase and PostgREST | Connect as an ordinary Postgres user, enforce in app code |
| Storage and Realtime | Both read the same JWT | Replace both services, not just auth |
The third row is the one that surprises people. Those roles are a PostgREST construct, not a Postgres feature you can recreate on another host. If you move to Neon, your application connects as a normal database user and none of the JWT-derived role switching exists. Row level security itself is a Postgres feature and still works, but the mechanism that populated the claims is gone, so the policies have nothing to read.
Plan to move authorization up into your application at the same time. That is a real rewrite and it is the reason Path A is worth taking seriously first.
Moving the passwords
Your users do not need to reset their passwords. Supabase Auth stores standard bcrypt hashes with a per-user salt in auth.users.encrypted_password. The column name says encrypted, which is a misnomer kept for backward compatibility. Any system that verifies bcrypt accepts them directly.
There is no export button in the Supabase dashboard. You pull the rows yourself:
select id, email, encrypted_password, email_confirmed_at, created_at
from auth.users
where deleted_at is null;Or take the schema with pg_dump --schema=auth. Either way, that output is a credential file. Do not put it in a ticket, a shared drive, or a chat message, and delete it once the import is verified.
Auth0 imports bcrypt through its bulk user import job. If you are landing on Firebase instead, firebase auth:import takes --hash-algo=BCRYPT with no hash key needed, which is the single cleanest password migration available from Supabase.
For anything that cannot take the hash, fall back to lazy migration: verify against the old system on first login, then re-hash under the new scheme. Scoped to the accounts that actually log in, the blast radius is small. The same pattern appears in Migrate Off Auth0, and it is provider-agnostic.
Auth0 or MojoAuth
Both give you 25,000 monthly active users free, which is the number most comparison posts still get wrong for Auth0. The old 7,500 figure is stale.
Auth0 is the answer if you want Path A, because it is on Supabase's supported issuer list and the integration is documented. Its free plan covers unlimited social connections, one enterprise connection, self-service SSO, and SCIM. It does not include MFA, per-organization role based access control, or separate production and development environments. That MFA gap matters: Supabase Auth includes MFA on its free tier, so for that one feature, leaving is a downgrade unless you qualify for Auth0 for Startups, which gives a year of B2B Professional at 100,000 MAU.
MojoAuth is the answer if you want passwordless as the default and you are bootstrapped. Its free tier covers email OTP, magic links, and social login for 25,000 monthly active users, with unlimited total registered users. Its startup perk asks only that you are under five years old and under $10 million raised. There is no venture-backing requirement, which is unusually open in this category. Passkeys, MFA, SMS OTP, custom domains, and enterprise SSO all sit on Business Pro at $50 per month.
The deciding factor is structural rather than about features. MojoAuth is not on Supabase's supported issuer list, so Path A is not a documented integration. You would be meeting the four JWT requirements yourself and validating it without a vendor guide. If you are staying on Supabase Postgres, that difference is worth more than any feature comparison. If you are leaving Supabase entirely, it does not matter at all.
For the wider provider landscape, see Supabase alternatives compared and how to choose a CIAM.
Validate before you trust it
Check four things at every stage, and check them against real login traffic rather than a test tenant.
- Policies still deny what they denied before. Write a test that logs in as user A and tries to read user B's rows. Run it before and after. A migration that silently opens a table is worse than one that fails loudly.
- The `role` claim is present. Decode a real production token and confirm it says
authenticated. This is the single most common Path A failure. - Token claims match what your app expects. Compare a Supabase-issued token and a provider-issued token side by side for the same user. A missing claim breaks authorization quietly.
- Storage and Realtime still authorize. Both read the same token, and both are easy to forget until a customer reports a broken upload.
Keep the old path available until you have run a full billing cycle on the new one. Under Path A, rollback is pointing Supabase back at its own JWT secret, which is a configuration change rather than a data restore. That property is the reason to prefer it.
Key takeaways
- Decide whether you are moving the database before you compare providers, because that single question changes the migration from a config change into a rewrite.
- The missing role claim is the most common failure in a third-party auth setup, and it fails by denying everything rather than by erroring.
- Audit for policies written as auth.uid() = user_id without a null check, because an unauthenticated request makes auth.uid() null.
- Never read raw_user_meta_data in a policy; it is user-writable. raw_app_meta_data is the immutable one.
- Treat the exported hash file as a credential, because that is exactly what it is, and delete it once the import verifies.
- Auth0's free plan has no MFA while Supabase's does, so for that one feature leaving is a downgrade unless you qualify for Auth0 for Startups.
Frequently asked questions
- Can I keep Supabase Postgres and change only the auth provider?
- Yes. Supabase's third-party auth integration accepts external providers as JWT issuers, and Clerk, Firebase Auth, Auth0, AWS Cognito, and WorkOS are officially supported. Your tables, row level security policies, Storage, and Realtime keep working because PostgREST verifies the external token against the provider's JWKS instead of Supabase's own secret.
- What does Supabase require from an external JWT issuer?
- Four things, all mandatory. Asymmetric signing, because symmetrically signed tokens are not supported. An OIDC discovery URL so Supabase can fetch the key set. A kid header on every token so the right key can be selected. And a role claim set to 'authenticated', which is what PostgREST uses for its SET LOCAL ROLE.
- Do my users have to reset their passwords?
- No. Supabase Auth stores standard bcrypt hashes with a per-user salt in auth.users.encrypted_password, despite the column name suggesting encryption. Any bcrypt-verifying system accepts them directly. Firebase is the simplest destination because firebase auth:import takes --hash-algo=BCRYPT with no hash key.
- What breaks if I drop Supabase Auth entirely?
- Foreign keys pointing at auth.users(id) fail once that schema stops being populated, and that pattern is close to universal. RLS policies calling auth.uid() lose their source. The anon, authenticated, and service_role roles are a PostgREST construct that does not exist elsewhere. Storage and Realtime both read the same JWT, so they need replacing too.
- Auth0 or MojoAuth?
- Both give 25,000 free monthly active users. Auth0 is on Supabase's supported issuer list, so the keep-your-database path is documented, and its startup program gives a year of B2B Professional at 100,000 MAU. MojoAuth leads with passwordless and its startup perk has no venture-backing requirement, but it is not a supported Supabase issuer, so that path is undocumented.