Firebase Authentication → Supabase
Export Firebase users with auth:export, copy the project's scrypt parameters, and create each person in Supabase Auth with a $fbscrypt$ hash. Sessions stay behind. The community SQL import does not bring the passwords with it.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
Firebase Authentication
- Simple SDKs, familiar social providers, and close Firebase integration accelerate web and mobile sign-in
- Centralized sign-in, federation, MFA, and user lifecycle controls reduce application security work
- Authorization, enterprise identity, tenant design, and advanced migration controls are less comprehensive
- Passwords, sessions, factors, identities, and authorization policy are unusually sensitive to migration errors
Supabase
- PostgreSQL, authentication, storage, realtime, and edge functions offer an open backend stack
- Managed infrastructure removes a large amount of operational work
- Production scaling and service integration still demand database and infrastructure judgment
- Platform-specific services can create architectural lock-in
Supabase: PostgreSQL, authentication, storage, realtime, and edge functions offer an open backend stack. This removes a major source-side concern: Authorization, enterprise identity, tenant design, and advanced migration controls are less comprehensive.
What you lose: Simple SDKs, familiar social providers, and close Firebase integration accelerate web and mobile sign-in. What you inherit: Production scaling and service integration still demand database and infrastructure judgment.
Jump to a section
Know the shape of the move.
This timeline assumes
- You can run firebase auth:export as a project Editor or Owner. The file-format table says the password hash and salt require that role.
- You can open Authentication, Users, the three-dot menu, and Password hash parameters, then store base64_signer_key, base64_salt_separator, rounds, and mem_cost outside the repo.
- Firebase Auth stays up through the rollback window. You can still sign a test user in there.
- Firestore, Storage, and Cloud Functions are a different move. That one is Firebase → Supabase. This route is the directory.
- The site already documents Firebase Authentication → Clerk and Firebase Authentication → Auth0, if you want a hosted login product instead of Supabase Auth.
What survives the move.
“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.
| Item | Outcome | Impact | What happens | Mitigation |
|---|---|---|---|---|
| Firebase scrypt passwords | partial | critical | auth:export writes passwordHash and salt only for passwords hashed with Firebase's modified scrypt. Supabase Auth accepts that hash on createUser as password_hash, in the $fbscrypt$ form its parser expects. New passwords Supabase creates itself are bcrypt. | Copy the four hash parameters from the console before you import. Sign in as a user whose password you know after the row lands. |
| Any other password hash | manual | critical | Firebase's export docs say other algorithms leave passwordHash and salt empty. Those accounts were usually imported from somewhere else, and Firebase only re-hashes them with scrypt on the next sign-in. | List every empty hash. Send a reset, or keep Firebase as the checker until that person has signed in once. |
| The community SQL import | lost | critical | import_users.js inserts an empty encrypted_password and stores the whole user object in raw_user_meta_data. Its provider switch only names Google and Facebook. GitHub and the rest are written down as email. The password middleware in that repo is still marked in progress. | Use the admin API for the real import. Treat a successful SQL load as a directory sketch, then check that a known password works. |
| Firebase uid as the primary key | partial | critical | Supabase user ids are UUID v4. createUser can take an id only in that form. A Firebase localId is not one. Tom at Minimum Code got ID must conform to the uuid v4 format when he tried to keep the old id. | Let Supabase assign the id. Store localId on app_metadata, and map it before you move any row that points at the old uid. That data move is Firebase → Supabase. |
| Sessions and refresh tokens | lost | high | A Firebase refresh token is not a Supabase session. People sign in again, unless you keep Firebase Auth as a third-party provider and pass its JWT through as the Supabase access token. | Ship the client that can verify a Supabase session before you disable a provider. Or register Firebase as a third-party auth integration and leave sign-in where it is for now. |
| Social logins | partial | critical | The export's providerUserInfo lists each linked provider and its rawId. The documented providerId values include google.com, facebook.com, github.com, and twitter.com. Automatic linking attaches a new OAuth identity when the email matches an existing confirmed user. SAML users are not linking targets. | Copy the OAuth client id and secret into the Supabase provider. Confirm the imported email first. Sign in once with each provider you still offer. |
| Two Firebase accounts, one email | manual | high | Supabase keeps one user per email so automatic linking can find them. Firebase can hold separate accounts on that address, and linking those was one of the pains Analyst Builder described. | Pick the surviving localId before createUser. Write down the one you skip. |
| Custom claims and roles | manual | high | user_metadata is editable by the signed-in user, and it is copied into the access token. app_metadata is service-role only, and it is in the token too. Tom's test user set role admin on his own user_metadata after the community import put the Firebase record there. | Put claims you do not want edited into app_metadata. Do not put the hash string or the signer key in either object. |
| Phone numbers | partial | high | The export includes phoneNumber. createUser can take phone and phone_confirm. The community SQL import writes phone as null. | Pass the phone on createUser and set phone_confirm when Firebase already verified it. Send one test code. |
| Anonymous users | lost | high | An anonymous Firebase user has no email, no phone, and no password. createUser needs an email or a phone, so that session does not become a Supabase user. | Count them. Keep Firebase Auth in front, via the third-party integration, until those sessions can die, or accept that they start over. |
| MFA factors | manual | high | The documented auth:export fields do not include a TOTP secret. There is no hash-style import for an enrolled factor. | Have one enrolled user add a factor on Supabase before you tell the list. Keep the Firebase check until that path works. |
| Templates, blocking functions, and tenants | manual | high | Action URLs, email templates, and blocking functions do not come across in the user file. Identity Platform tenants are separate user pools. The documented auth:export command takes a file and a format, and it has no tenant flag. | List tenants in the console. If you have any, export them with the Admin SDK for that tenant id. auth:export alone is the default pool. Rebuild each template and hook, and send one mail from Supabase before cutover. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| localId | app_metadata.firebase_uid | manual | Supabase assigns a UUID. Keep the Firebase id beside it. |
| passwordHash and salt, Firebase scrypt | auth.users.encrypted_password via password_hash | transform | Build $fbscrypt$v=1,n=<mem_cost>,r=<rounds>,p=1,ss=<salt separator>,sk=<signer key>$<salt>$<passwordHash>. |
| Empty passwordHash | No destination | unsupported | Reset, or check the password against Firebase on the next login and then set it. |
| email and emailVerified | email and email_confirm | transform | Set email_confirm when Firebase already verified the mailbox. Linking looks for a confirmed email. |
| phoneNumber | phone and phone_confirm | transform | The community SQL import leaves phone null. Pass it yourself. |
| displayName and photoUrl | user_metadata | transform | The person can change this later. Keep it small. |
| providerUserInfo rawId | auth.identities after sign-in or linkIdentity | automatic | Same OAuth client id avoids a fresh consent screen. Test one linked account. |
| Custom claims | app_metadata | manual | Not a documented column in the export file. If your JSON has them, they do not belong in user_metadata. |
| Anonymous user | No destination | unsupported | No email and no phone, so there is nothing for createUser to keep. |
| Sessions and refresh tokens | No destination | unsupported | People sign in again, or you keep passing the Firebase JWT. |
Make the move recoverable.
Create the source-of-truth backup
Keep an export you can re-read, and keep the hash parameters off the app repo.
- Count users in the Firebase console, split by provider.
- Download the password hash parameters and store them with the same care as a private key.
- Run firebase auth:export users.json --format=json from an Editor or Owner account. The .json suffix selects the format.
- If the project uses Identity Platform tenants, list them in the console. auth:export has no tenant flag, so a tenant export is a separate Admin SDK call. Label each file with the tenant id.
- Store the JSON outside the repo, with the counts and the account that ran the export.
Proof to capture: The console count, the export file, and a note that passwordHash is present for the scrypt users you expect.
Build the hash string from the export and the console parameters
createUser will only check a Firebase password if the hash uses the $fbscrypt$ form.
- Read mem_cost, rounds, base64_salt_separator, and base64_signer_key from the Password hash parameters dialog. Firebase's sample config shows SCRYPT, and most projects use that modified algorithm.
- For each user with both passwordHash and salt, build $fbscrypt$v=1,n=<mem_cost>,r=<rounds>,p=1,ss=<salt separator>,sk=<signer key>$<salt>$<passwordHash>. The Auth parser treats n as that mem_cost value and r as rounds. p has to be at least 1. Use 1 for Firebase's own scrypt.
- Call createUser with the service role, on a machine that is not the browser. Pass email, password_hash, and email_confirm true when emailVerified is true. Pass phone and phone_confirm the same way. Put localId in app_metadata.
- Do not also pass password. Do not put the hash or the signer key into user_metadata or app_metadata. Both objects are copied into the access token.
- Skip rows with an empty passwordHash. They go on the reset list, or through a Firebase sign-in check on the next attempt.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
Keep Firebase as the login only if you are not ready to move it
Supabase can trust a Firebase JWT while the directory still lives in Firebase.
- In the Supabase dashboard, add a Firebase third-party auth integration with the Firebase project id.
- Give each Firebase user a custom claim role set to authenticated. Without it, Supabase treats the JWT as anon.
- Pass the Firebase ID token from accessToken on the Supabase client. Call getIdToken(true) after a blocking function sets that claim, because onCreate is not synchronous.
- On the hosted platform, Supabase rejects JWTs from Firebase project ids you did not register. Firebase uses one set of signing keys for every project. If you self-host, add a restrictive RLS policy that checks the issuer, or an unrelated Firebase project can reach your tables.
- This keeps the current sessions. It does not move the users. Come back to createUser when you want Supabase to be the password store.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
The things most likely to hurt.
These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.
The SQL import looks finished and nobody can sign in
import_users.js writes an empty encrypted_password. Tom ran it on three emulator users and the old password returned Invalid login credentials.
- Consequence
- You cut over onto a directory that has emails and no passwords. Analyst Builder hit this class of gap in production and asked all 7,000 beta users to reset.
- Mitigation
- After the load, sign in as the test user. If the hash path is not ready, send the reset while Firebase can still sign people in.
Stop if: A known password fails and the reset mail is not ready.
The signer key rides along in the token
The community script JSON.stringifies the user into raw_user_meta_data. Tom's test user then edited that object with his own token and granted himself role admin.
- Consequence
- A password hash, a salt, or a project signer key can end up in every access token, and the person can change the claims you stored there.
- Mitigation
- Strip the export down to name and photo for user_metadata. Put firebase_uid and roles in app_metadata only.
Stop if: A decoded access token contains passwordHash, salt, or base64_signer_key.
The rehearsal used the emulator
Tom says the emulator stores fake hashes. He had to use the sample vector from Firebase's scrypt repository to get a real check.
- Consequence
- The pilot passes on a fake hash, so your project's signer key, rounds, and mem_cost never get checked.
- Mitigation
- Pilot against a copy of the production export, or against that published vector plus your own parameters on a staging project.
Stop if: The only successful password check used an emulator hash.
A wrong clock looks like a failed migration
After Analyst Builder moved, some customers were logged out immediately. Kasun traced it to the machine clock GoTrue uses when it creates the token.
- Consequence
- Support treats a client clock as a bad import, and people who just reset a password think the reset failed.
- Mitigation
- Sign in from a device with a correct clock, and from one you have set a few minutes off, before you call the cutover done.
Stop if: A test device with a correct clock is signed out and the password error does not explain it.
Do the work in this order.
- Day 1 · inventory
Count providers and copy the hash parameters
2 to 4 hours active1 day elapsedNone waiting- Count email, phone, social, and anonymous users.
- Save the four hash parameters outside the repo.
- List OAuth client ids you will reuse.
- List tenants, blocking functions, and templates you will rebuild. A tenant is not in the auth:export file.
Stop / go checkpointCan this project export hashes?
Go when: An Editor or Owner can run the export, and the hash parameters are stored.
Stop when: The account that can export is not available, or you cannot tell which tenant is production.
- Days 1 to 2 · export
Export and separate the empty hashes
Half a day active1 to 2 days elapsedThe export command waiting- Run firebase auth:export users.json --format=json.
- Count rows with a passwordHash, rows without one, and duplicate emails.
- Pick the surviving account where two localIds share an email.
Depends on: inventory
Stop / go checkpointIs the file the directory you think it is?
Go when: The count matches the console, and every duplicate email has a survivor.
Stop when: Every passwordHash is empty and the role you used was not Editor or Owner.
- Day 2 or 3 · pilot
Create a handful of users
Half a day active1 day elapsedNone, unless you are waiting on a staging project waiting- createUser for one scrypt user, one social user, one phone user, and one duplicate-email pair.
- Sign in with the known password.
- Decode the access token and confirm it has no hash and no signer key.
Depends on: export
Stop / go checkpointDoes the hash check work on this project's parameters?
Go when: The known password returns a session, and a wrong password does not.
Stop when: The only passing check used an emulator hash, or the token contains the signer key.
- After the pilot · clients
Ship a client that can take a Supabase session
A few days activeA few days, longer if a store reviews the build elapsedApp review, if you ship a native app waiting- Verify a Supabase access token on the API.
- Keep the Firebase path until that build is what people are running.
- If you are not moving passwords yet, wire the third-party Firebase JWT instead and stop here.
Depends on: pilot
Stop / go checkpointCan a production build sign in with Supabase?
Go when: The build people already have accepts a Supabase session, or it still sends a Firebase JWT on purpose.
Stop when: Only a local build can reach Supabase.
- Cutover day · backfill
Import the rest and turn providers over
A day active1 day elapsedNone if the export is fresh waiting- Export once more and import anyone created after the pilot.
- createUser for the remaining scrypt users.
- Point each social provider at Supabase. Leave Firebase up for the empty-hash list.
Depends on: clients
Stop / go checkpointIs every password accounted for?
Go when: Every scrypt user can sign in, or is named on the reset list.
Stop when: The reset mail is not ready and the empty-hash list is not empty.
Cut over with a way back.
Cutover
New sign-ins go to Supabase, and Firebase can still check a password that had no hash.
- Stop new Firebase signups. Write down the time.
- Export once more and create anyone who appeared after the last file.
- Turn each social provider to Supabase. Keep Firebase able to check a password for the empty-hash list.
- Send the reset only to that list. Do not send it to people whose $fbscrypt$ login already worked.
- Leave the Firebase project billed. Do not delete the users this week.
Proof to capture: The signup freeze time, a scrypt login, one social login, and the reset list with a count.
Rollback
Send sign-in back to Firebase without throwing away the Supabase rows you may need to diff.
- Point the login path back at Firebase.
- Leave the Supabase users in place.
- If someone signed up only on Supabase during the overlap, write that id down before you turn the new signup off.
- Read one Firebase login before you call the rollback done.
Proof to capture: A production login through Firebase, and a list of accounts that exist only in Supabase.
- The known-password test user cannot sign in on Supabase.
- A social login creates a second app profile.
- A decoded access token contains the signer key or a password hash.
- The installed client cannot verify a Supabase session, and you have already disabled Firebase sign-in.
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
USR-01Blocking | Counts by provider | Compare the Firebase console and the export with auth.users, split by password, phone, social, and anonymous. | Counts match, or the exception list names every skip. | The count table. | |
PWD-01Blocking | A known scrypt password still works | Sign in on Supabase as the test user whose password you set in Firebase. | The session is issued and the app loads the old profile. | The user id and the time of the login. | |
HSH-01Blocking | Empty hashes are listed | Diff the export for rows with no passwordHash. | Each one is on the reset list or still checked by Firebase. None are silent. | The diff and the reset list. | |
UID-01Blocking | localId maps to the new UUID | Open app_metadata.firebase_uid for the pilot users and join it to your app tables. | Every app row you kept points at the new id, through that map. | The map sample. | |
SOC-01Blocking | Each provider hits the old profile | Sign in with each social provider you still offer. | The app user is the imported one, not a second profile. | The provider id and the Supabase user id. | |
DUP-01Blocking | Shared emails have a decision | Open the duplicate-email list and the Supabase user each one became. | Each pair has a surviving account. | The decision list. | |
TOK-01Blocking | The access token is safe to log | Decode a session token for an imported user. | No passwordHash, salt, or signer key. Roles you care about sit in app_metadata. | The decoded claim names, not the token itself. | |
PHN-01Blocking | A verified phone stays verified | Sign in as a user Firebase already verified by phone. | Supabase does not ask them to verify that number again. | The user id. Skip only if the project has no phone users, and write that down. | |
APP-01Blocking | The API accepts the new token | Call a signed-in route with a Supabase access token for a migrated user. | The route returns the same app profile a Firebase token returned. | The response and the user id. |
Remove the scaffolding safely.
Safe after: A week of Supabase sign-ins, with the empty-hash list either mailed or still checked by Firebase on purpose.
- Delete the export and the hash-parameter file from laptops and CI once you are sure you will not rerun the import.
- Revoke the service-role key you used for createUser, and the Firebase private key if you generated one for the community script.
- Remove the Firebase JWT check only after the rollback window, and only if you are not keeping the third-party integration.
- Keep the Firebase project until finance has the last invoice and the reset list is done.
- Set the next review date. The data move, if you still have one, stays on Firebase → Supabase.
When the plan met reality.
First-hand accounts are preferred. Vendor case studies are labeled, and every note below is an editorial paraphrase—follow the link for the full context.
Kasun Vithanage presented Analyst Builder's move at a Supabase meetup in August 2024. The product was a closed beta of 7,000 users. They adapted a script from the Supabase team, imported the users with SQL, filled a user_providers table, and dropped the existing passwords. Beta users were asked to reset. A custom x-auth-provider header let the app talk to Firebase and Supabase during the overlap. He describes GitHub login failing for some people on Firebase, painful identity linking, and no auth audit logs. After the move he says 99% of login-related issues were gone and LinkedIn sign-in was available. Some customers were then logged out immediately. He traced that to the machine clock GoTrue uses when it creates a token, and fixing the clock fixed the logout. A later slide says they reached 50,000+ with Supabase. That figure is not a count of imported accounts.
- If the import leaves passwords behind, send the reset while Firebase can still sign people in, and keep a flag that can accept the old token.
- Test a same-email account before you promise linking. He called Firebase's linking painful, and the new side only worked once each person was one user.
- A SQL import of the directory was not a password import. They chose a reset for the whole beta of 7,000.
- The failure he still hit was clocks. A wrong client clock looked like an instant logout after an otherwise successful move.
Tom, founder of Minimum Code, writes that this page is not a client migration. On 6 October 2026 he built a three-user project tracker in the Firebase emulators and moved it to local Supabase Auth v2.197.0. The community import_users script inserted all three with an empty password, so the old password returned Invalid login credentials. He then created a user through the admin API with a $fbscrypt$ hash, using the sample vector from Firebase's scrypt repository because the emulator stores fake hashes. The right password returned HTTP 200 and a session. A wrong one was refused. After that login the stored hash was still the Firebase string. Setting the Firebase uid as the Supabase id failed with ID must conform to the uuid v4 format. The community import had also copied the user record into user_metadata, and a normal user could change it, including giving himself role admin.
- Rehearse the hash import with a production export or Firebase's published test vector. An emulator export does not contain a real scrypt hash.
- Keep the signer key and the hash out of user_metadata. In his run a signed-in user edited that object, including a role.
- The community SQL import he ran does not fill encrypted_password. The admin password_hash path did, on local Auth 2.197.0, and that login did not rewrite the hash to bcrypt.
- The community scripts crashed on Node 25. Node 22 ran them. npm ci failed on the repo lockfile, and npm install worked.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Supabase: migrate from Firebase AuthAccessed 2026-11-08
- supabase-community/firebase-to-supabase auth READMEAccessed 2026-11-08
- supabase-community/firebase-to-supabase import_users.jsAccessed 2026-11-08
- Firebase: auth:import and auth:exportAccessed 2026-11-08
- Supabase Auth: AdminUserAttributes.password_hashAccessed 2026-11-08
- Supabase Auth: Firebase scrypt parserAccessed 2026-11-08
- Supabase: Firebase Auth as a third-party providerAccessed 2026-11-08
- Supabase: identity linkingAccessed 2026-11-08
- Kasun Vithanage: Analyst Builder's journey from Firebase to SupabaseAccessed 2026-11-08
- Tom at Minimum Code: Firebase to Supabase, passwords includedAccessed 2026-11-08