Supabase Auth Migration Scope — Which Tables to Migrate (and Which to Skip)
If you're moving a production Supabase project to another Supabase project — Lovable Cloud to your own, a self-hosted instance to managed, or just consolidating accounts — the question that decides whether users have to reset their passwords is: which auth tables come along, and in what order.
TL;DR
- Migrate:
auth.users(withencrypted_passwordpreserved) andauth.identities. Both are required for any sign-in to work — not just OAuth. - Add if applicable:
auth.mfa_factorswhen users have TOTP/WebAuthn enrolled. - Re-enter manually: OAuth provider client IDs and secrets (Google, GitHub, Apple) — they're in Supabase's auth control plane, not the database.
- Do not touch:
auth.schema_migrations,auth.instances,auth.flow_state,auth.one_time_tokens. - Sequence: restore
auth.usersfirst, thenauth.identities(FK dependency), then clean any seeded public rows, then restore public data in FK order.
Why both tables are required, even for email + password
The most common misconception is that auth.identities is just for OAuth — Google, GitHub, Apple sign-ins. It used to be. Since GoTrue v2 (Supabase's auth service), every sign-in path looks up auth.identities to discover which providers a user has linked, including email and phone. When you create a password user via the Admin API or the signup flow, Supabase quietly inserts an identity row with provider='email' and a JSON blob pointing back to auth.users(id).
If you migrate auth.users but skip auth.identities, every sign-in fails — even though the bcrypt hash is sitting right there in auth.users.encrypted_password. GoTrue refuses to authenticate a user whose identity row it can't find. This is also why the "create a fake user and sign in as them" trick requires you to insert an auth.identities row by hand — without it, Supabase doesn't believe the user can authenticate at all.
So the minimum-viable auth migration is exactly the two tables forum posts converge on — but the framing "users for the hash, identities for OAuth" is wrong. Both are load-bearing for both sign-in types.
The OAuth control-plane gap
Copying auth.identities perfectly still doesn't get your Google sign-in users back. Provider client IDs, secrets, redirect URIs, and JWT secrets all live in Supabase's auth control plane (the settings you configure under Authentication → Providers), not in any table you can pg_dump. The target project has none of this until you re-enter it manually.
Result: an OAuth user lands at the new project, the identity row exists, but the login fails with "Unsupported provider" because Google isn't enabled on the target. The fix is dashboard work, not SQL work — open the target's auth settings, enable each provider used by the source, paste the client ID and secret. If the source used a custom redirect URL, update it in the Google Cloud Console too, since the target lives on a different Supabase URL.
Do this before you flip DNS or app config to the new project.
Otherwise the first user who tries Google sign-in after switchover sees the error, and you'll be chasing support tickets for the next hour. Configure providers in the target while the source is still live.
MFA: a third table, only if anyone enrolled
If users have enabled TOTP or WebAuthn factors, their factor records live in auth.mfa_factors. Skip it and those users get prompted to re-enroll their authenticator app on first sign-in (which is mildly painful but not catastrophic). For a paid SaaS where security-minded users opted into MFA, migrating this table is worth the extra five lines of SQL.
The two siblings — auth.mfa_challenges (pending challenges) and auth.mfa_amr_claims (session-bound claims) — are session-scoped, short-lived, and safe to skip. They regenerate on the next sign-in attempt.
Tables to explicitly NOT migrate
| Table | Why not |
|---|---|
| auth.schema_migrations | GoTrue maintains its own migration history in the target. Overwriting it causes future GoTrue version bumps to refuse to apply, because the recorded history doesn't match reality. |
| auth.instances | Single-tenant on hosted Supabase. The target manages this. Copying is a no-op at best, an integrity violation at worst. |
| auth.flow_state, auth.one_time_tokens | PKCE state and short-lived password-reset/email-confirm tokens. Their TTL is minutes. By the time the migration finishes they've expired anyway. |
| auth.refresh_tokens, auth.sessions | Session continuity would require also migrating the JWT signing secret (different per project). Almost no one does this — accepting a one-time re-login is the standard tradeoff. |
| auth.audit_log_entries | Historical, potentially huge, never read by any auth flow. Migrate only if you need it for compliance reasons that can't be served by snapshotting the source separately. |
Column-parity drift — the bug that doesn't show up in dry-runs cleanly
Supabase periodically adds columns to auth.users and auth.identities as GoTrue (the underlying auth service) ships new features. If source and target are on different GoTrue versions — which they almost always are when migrating from a long-lived Lovable Cloud project to a freshly-provisioned Supabase project — an INSERT INTO ... SELECT * can break in either direction: the source may have columns the target doesn't recognise, or the target may have NOT NULL columns the source dump never populated.
Two ways to avoid surprises:
pg_dump --column-insertsemits explicitly-named column lists per row, so Postgres handles missing columns on the target side with defaults instead of blowing up.- Before the production run, do a
\d auth.usersand\d auth.identitieson both source and target. Diff the column lists. Resolve any required-not-null columns explicitly (most can default toNULL,false, or a sane filler).
A clean dry-run against a freshly-provisioned target project catches this — but only if the dry-run target is on the same GoTrue version as the eventual production target. If you dry-run against a project created last month and provision the prod target the day of the cutover, you can hit drift that didn't show up in the dry-run.
The sequence that actually works
- Apply schema (your tracked migrations) into a fresh target.
- Restore
auth.userswithencrypted_passwordintact. - Restore
auth.identities— FK depends on step 2, so order matters. - (Optional) Restore
auth.mfa_factors. - Reconfigure OAuth providers in the target dashboard.
- Delete any seeded/default public rows the fresh target created (e.g. demo data inserted by your migrations).
- Restore
publicdata in FK-safe order. Public tables withcreated_by/owner_id/user_idcolumns referenceauth.users(id)— they'd FK-violate if auth wasn't first.
This is the same ordering Staticbot's pipeline runs internally. The "public FK to auth.users" detection is universal — almost every Supabase app has at least one such column, and it's why "import data first, fix auth later" never works.
Want this automated end-to-end?
Staticbot's Supabase migration pipeline does all of the above: dumps auth.users with encrypted_password, applies auth.identities in dependency order, sequences the public-schema import after auth, and surfaces the OAuth-provider-config gap as a manual step in the migration UI (because there's no way around the control-plane). Pricing is $19 per migration after the free first one. The pipeline works for Lovable Cloud, Bolt, Base44, v0, and self-hosted Supabase as either source or target.
