auth schema of your own Postgres database. That makes the export easy: you run SQL, not a support ticket. This page covers moving the users. For the code side, see the concept map in Migrate from Clerk, which applies here too, and Run both and migrate for the rollout.
Password hashes: what carries over
Supabase stores a bcrypt hash inauth.users.encrypted_password. Supabase’s own users receive it as a modular crypt string starting with $2a$10$. WorkOS documents the same fact in its Supabase migration guide, and notes you can read the hashes yourself because you have database access.
The importer keeps the hash as it is. Lazy migration checks it on the user’s first sign-in and rehashes it with theAuth’s own algorithm, so users keep their passwords. theAuth does not bundle a bcrypt implementation: pass a verifiers.bcrypt function (for example from bcryptjs) as shown on that page. Without it, those users are sent through a password reset.
Limits, stated plainly:
- Only rows whose
encrypted_passwordis bcrypt or argon2 are imported with a hash. Anything else is imported without one and reported asUNSUPPORTED_HASHER. - Users who only ever signed in with OAuth or a magic link have an empty
encrypted_password. They are imported without a hash and sign in the way they did before. - The Supabase Admin API (
auth.admin.listUsers) does not return hashes. You need a direct database connection, or a dump of theauthschema. - We built the fixtures from the documented column names, not from a live Supabase project. Run
theauth migrate verifywith one real account before you trust a full batch.
Step 1: export
Run this in the Supabase SQL editor or any Postgres client. It produces one JSON array.supabase-users.json. A CSV export also works. The importer accepts either the one row per user shape above, or the join shape that Supabase and WorkOS show in their docs: auth.users left joined to auth.identities, which gives several rows per user with provider and provider_id columns. Rows are merged by user id.
Treat the file like a credential store. It holds password hashes. Keep it off shared drives and delete it after the import.
Step 2: dry run, then import
What maps
Not imported: sessions and refresh tokens (users sign in once more), MFA factors, one time tokens, and users with no email (phone only accounts are reported as
MISSING_FIELD). Row level security policies that reference auth.uid() are database code and stay yours to port.
Cutover
- Import, then run
theauth migrate verify --from supabase ./supabase-users.json --db ./theauth.db --sample-email you@yourcompany.comwithTHEAUTH_MIGRATE_PASSWORDset. - Point sign-in at theAuth. Supabase JWTs are signed with your project secret and cannot be accepted as theAuth sessions, so everyone signs in once.
- Update OAuth app redirect URIs from
https://<project>.supabase.co/auth/v1/callbackto theAuth’s callback. - Keep the Supabase project read only for a few weeks, then remove the
authschema copy of the hashes.
Related
Import users
The importer, dry runs, conflicts and the CLI.
Lazy password migration
How bcrypt hashes become native hashes on first login.