BuildBaseBuildBase

Import your users from another service

Move your users from Clerk, Auth0, Firebase or Supabase with one command, keeping their passwords, phone numbers, old IDs and Google or Apple sign-ins.

When you move to BuildBase from another sign-in service, your users can come with you. The SDK's command line tool reads the export file that Clerk, Auth0, Firebase or Supabase gives you, and creates each person in BuildBase with their email address, name, password, phone number, their ID in the old system and the Google, GitHub, Microsoft, Apple or LinkedIn sign-ins they used. Nobody has to reset a password: at their next sign-in, people type the password they always had, or press the Google button they always pressed. This guide moves three people from a Supabase project into Acme Analytics.

Before you start

  • The export from your old service, and for some services its password hashes too. See Get the export.
  • Node.js 18 or later on the computer you run the command on, to run npx.
  • An API key for your BuildBase organization that can create users. Step 3 makes one.
  • The sign-in methods your users had, set up first: email and password, and each social provider, such as Google or Apple, ticked on your app.
  • Room on your plan: imported people count toward its end-user limit, like anyone who signs up.

What comes across

From the old systemIn BuildBase
Email addressThe account's address. A record without one is not imported.
Verified or notKept. A verified address is not asked to verify again.
NameThe account's name.
The user's IDKept on the user as externalId, so your own data can find the new account.
Password hashChecked at the next password sign-in with the old system's algorithm. Nobody sees or sends the password itself.
Phone numberThe account's phone, and whether it was verified, for phone sign-in.
Social sign-insGoogle, GitHub, Microsoft, Apple and LinkedIn, connected to the account.

Sessions, passkeys and authenticator apps do not come across: people sign in once more, and add a passkey or two-step sign-in again if they want one. A passkey belongs to the domain it was made on, so it could not move anyway.

1. Get the export

FromHow to export--from
SupabaseQuery the auth.users table and save the result as JSON or CSV, with each user's rows from auth.identities to keep their social sign-ins (below). Passwords are bcrypt and come across as they are.supabase
ClerkThe CSV from the Clerk Dashboard has password hashes but no social sign-ins. To keep both, list users through Clerk's Backend API as JSON, and pass the dashboard CSV with --passwords.clerk
Auth0A bulk user export job, as JSON (it may arrive gzipped; the command reads it either way). Auth0 never puts password hashes in it: ask Auth0 support for the password hash export and pass it with --passwords.auth0
Firebasefirebase auth:export users.json --format=JSON with the Firebase CLI. The password hash parameters (signer key, salt separator, rounds, memory cost) are in the Firebase console, under Authentication.firebase

For Supabase, this query in the SQL editor returns what the command reads, with the identities as a column:

select u.id, u.email, u.email_confirmed_at, u.phone, u.phone_confirmed_at,
       u.encrypted_password, u.raw_user_meta_data, u.deleted_at,
       (select json_agg(json_build_object('provider', i.provider, 'provider_id', i.provider_id))
          from auth.identities i where i.user_id = u.id) as identities
from auth.users u;

Save it as supabase-users.json. The export used here has four people: Priya Shah, who signed in with a password and has a phone number; Jordan Lee, who signed in with Google; Morgan Diaz, who signed in with Apple and hid their address behind Apple's relay; and Casey Brooks, who deleted their account. The export holds password hashes, so keep it as safe as a password, and delete it when you are done.

2. Try it with a dry run

--dry-run converts the export and shows what would be sent, without a key and without sending anything:

npx buildbase import --from supabase supabase-users.json --dry-run
buildbase: read 4 records from supabase-users.json (supabase): 3 to import
note: Apple user ids are per team: imported Apple links match only if this app uses the same Apple developer team as the old system.

The first 3 users, as they would be sent (hashes redacted):
  record 1: {"email":"[email protected]","emailVerified":true,"name":"Priya Shah","externalId":"7d1c2b3a-1111-4a5b-8c9d-0e1f2a3b4c01","phone":"+14155550123","phoneVerified":true,"password":{"hasher":"bcrypt","hash":"<redacted>"}}
  record 2: {"email":"[email protected]","emailVerified":true,"name":"Jordan Lee","externalId":"7d1c2b3a-2222-4a5b-8c9d-0e1f2a3b4c02","links":[{"provider":"google","providerAccountId":"110022003300440055006"}]}
  record 3: {"email":"[email protected]","emailVerified":true,"name":"Morgan Diaz","externalId":"7d1c2b3a-3333-4a5b-8c9d-0e1f2a3b4c03","links":[{"provider":"apple","providerAccountId":"001234.0a1b2c3d4e5f60718293a4b5c6d7e8f9.1234"}]}

Not sent:
  record 4 ([email protected]): deleted: the user is deleted in Supabase, so they are not imported

buildbase: dry run, nothing was sent. 3 users would be sent in 1 batch.

Read it before you go on. Each user shows what BuildBase will receive: Priya's phone has its + back, her password is a bcrypt hash, and Jordan's and Morgan's sign-ins are links to Google and Apple. Casey is left out, because the account was deleted in Supabase. The note is about Morgan: Apple gives each developer team its own user IDs, so the Apple link matches only if your Apple method uses a Services ID in the same team as before. Otherwise Morgan is found by email address at their first Apple sign-in instead.

npx buildbase runs the SDK installed in your project. Without it, npx @buildbase/sdk import does the same. npx buildbase import --help lists every option.

3. Make an API key

The command signs in to BuildBase with an API key of your organization.

  1. In the console, open Settings at the bottom of the menu, then API keys under Organization.
  2. Press Create API key. Give it a Name, such as User import, and a Description. Leave Role on Your own role, or pick an API role that can create users; Settings, API roles makes one.

The Create API key dialog: Name User import, Description Moves our users over from Supabase, Expires at left empty, and Role left on Your own role

  1. Press Create, then Copy key. The key is your organization ID, a colon and a secret. It is shown only this once.

Copy your API key: the key, shown here as an example made of the organization ID, a colon and the secret, with a Copy key button, and the warning that it is shown only this once

When the import is done, deactivate or delete the key on the same page.

4. Run the import

Give the command your BuildBase server and the key in the environment. read -rs takes the key from a paste without showing it, so it stays out of your shell history:

export BUILDBASE_SERVER_URL=https://api.console.buildbase.app
read -rs BUILDBASE_API_TOKEN && export BUILDBASE_API_TOKEN
npx buildbase import --from supabase supabase-users.json

The server is https://api.console.buildbase.app{/* @endstat */} on BuildBase's cloud, or your own for a self-hosted one. The command sends the users in batches of 500 and prints the totals:

buildbase: read 4 records from supabase-users.json (supabase): 3 to import

buildbase: 3 created, 0 updated, 0 skipped, 0 failed (3 users sent, 1 not sent)
note: Apple user ids are per team: imported Apple links match only if this app uses the same Apple developer team as the old system.

Failed:
  record 4 ([email protected]): deleted: the user is deleted in Supabase, so they are not imported

Running it again is safe: people already imported are skipped, so a run cut short can simply be run again.

buildbase: 0 created, 0 updated, 3 skipped, 0 failed (3 users sent, 1 not sent)

--overwrite updates people who are already there instead of skipping them. It never changes the address someone signs in with, and never replaces a password they have set in BuildBase since.

The command ends with exit code 0 when every record was imported, 1 when any record failed or was not sent (as Casey's here), and 2 when it could not start, for example without a key. That matters if you run it from a script.

5. Check the users in the console

Open User management, then Users. The imported people are there, with Priya's phone number:

The Users list in the console after the import, with Priya Shah and her phone number, Jordan Lee and Morgan Diaz

An import is not a sign-up: nobody is sent a welcome or verification email, so tell people about the move yourself, when you are ready. Each person created emits the user.imported event, not user.registered, so a workflow or webhook that listens for sign-ups does not run for them; subscribe to user.imported to sync them to your own systems.

6. What people see at their first sign-in

With a password. Priya opens your sign-in page and types her email address and the password she had in Supabase:

The hosted sign-in page with priya.shah@acme.example and her password from the old system typed in, ready to Sign in

BuildBase checks it against the imported hash with the old system's algorithm. It matches, so she is signed in. Her verified address needs no code. After the Trust this device? question, she is in your app:

The app page after that first sign-in: Signed in as Priya Shah, priya.shah@acme.example, with no password reset and no verification email

From then on her password is stored the way BuildBase stores every password, and the old hash is gone. Someone whose old hash BuildBase cannot check is imported with no password, and the command says so: they sign in with a magic link, an email code or a social provider, or set a new password with Forgot password?.

With Google or GitHub. Jordan presses Google. Google gives a person the same user ID in every app, so BuildBase finds Jordan's account by that ID and signs them in, whichever Google client your method uses.

With Microsoft, Apple or LinkedIn. These give each app registration (Microsoft), developer team (Apple) or developer app (LinkedIn) its own user IDs. The imported link matches only while your method uses the same registration, team or app as the old system, and the command's note after the totals says so for each of them the export holds. With a new one, BuildBase finds the person by their verified email address instead, as for any first social sign-in. Someone who hid their address, like Morgan, is the exception: Apple gives each developer team its own relay address too, so with a new team neither the ID nor the address matches, and their first Apple sign-in makes a new account. Keep the old team for them.

Unverified addresses. Someone whose address was not verified in the old system is asked for an emailed code at their first sign-in, as a new sign-up would be.

With a phone. Priya's verified number works for phone sign-in at once. An unverified imported number becomes verified the first time a code sent to it signs the person in.

Inside your app, imported Google, Apple and other sign-ins show in Connected accounts on the SDK's Security screen, where people can remove one or connect another. Account security screens shows it.

Check it works

  • The dry run shows every user you expected, and nothing you did not.
  • The import reports no failed rows other than ones you meant to leave out.
  • The people are under Users.
  • One test person signs in with their old password, and one with each social provider they used.
  • The API key is deactivated or deleted, and the export file deleted.

Things to watch out for

  • Microsoft, Apple and LinkedIn sign-ins match only the same app. Microsoft gives each app registration, Apple each developer team and LinkedIn each developer app its own user IDs. Google and GitHub give the same ID everywhere. With a new registration, team or app, people are found by their verified address instead, and an Apple hidden address never matches.
  • Auth0 keeps password hashes out of its export. Ask Auth0 support for the password hash export before you plan the move, and pass it with --passwords.
  • An import is not a sign-up. Nobody gets an email, and each person emits user.imported, not user.registered, so a workflow or webhook that waits for sign-ups does not run for them.
  • The plan limit stops the rest. When your plan's end-user limit is reached, that person and every later new one fail with limit_reached. Raise the limit before a large import.
  • The export holds password hashes. Keep it as safe as a password, delete it when you are done, and deactivate the API key.

If something goes wrong

What you seeWhyWhat to do
buildbase: set BUILDBASE_API_TOKEN to an org API token allowed to create usersNo key in the environmentExport BUILDBASE_API_TOKEN as in step 4.
buildbase: set BUILDBASE_SERVER_URL (for example https://api.console.buildbase.app)No server in the environmentExport BUILDBASE_SERVER_URL, or pass --server-url.
buildbase: the API token was refused (401). Check BUILDBASE_API_TOKEN and that its role can create users.A mistyped, deactivated or expired key, or a role that cannot create users (403)Make a new key in API keys, with your own role or a role that can create users.
buildbase: --from is required: clerk, auth0, firebase, supabaseNo --fromAdd --from and the service the file came from.
buildbase: --passwords is for Auth0's hash export or Clerk's CSV, not supabase--passwords with another serviceLeave it out: Supabase and Firebase exports carry their hashes.
buildbase: read the JSON export: firebase auth:export users.json --format=JSON (the CSV has no Microsoft or Apple sign-ins)A Firebase CSVExport again as JSON.
skipped in the totalsThe person is already here, matched by email or old ID (exists)Nothing to do; --overwrite updates them instead.
A row with limit_reachedYour plan's end-user limit is reached. That person and every later new one failUpgrade, then run the same command again.
A row with deleted, invalid_email or plaintext_passwordThe old system had deleted them, the address is not one, or the file holds a password instead of a hashFix the export if they should come across; a plaintext password is never imported.
An imported Microsoft, Apple or LinkedIn person gets a new, empty account at first sign-inYour method uses a new app registration, team or app, and their address did not match, as with an Apple relay addressUse the old registration, team or app for your method.

Next steps