Skip to main content
Supabase Auth (the GoTrue service) keeps users in the 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 in auth.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_password is bcrypt or argon2 are imported with a hash. Anything else is imported without one and reported as UNSUPPORTED_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 the auth schema.
  • We built the fixtures from the documented column names, not from a live Supabase project. Run theauth migrate verify with 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.
Save the single cell as 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

From code:

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

  1. Import, then run theauth migrate verify --from supabase ./supabase-users.json --db ./theauth.db --sample-email you@yourcompany.com with THEAUTH_MIGRATE_PASSWORD set.
  2. 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.
  3. Update OAuth app redirect URIs from https://<project>.supabase.co/auth/v1/callback to theAuth’s callback.
  4. Keep the Supabase project read only for a few weeks, then remove the auth schema copy of the hashes.

Import users

The importer, dry runs, conflicts and the CLI.

Lazy password migration

How bcrypt hashes become native hashes on first login.
Last modified on October 19, 2026