Importing users
Move users from Auth0, Firebase, Cognito, Supabase, Clerk, Keycloak, Okta or your own database, with or without their passwords.
You can bring existing users to 9in without asking them to sign up again, and usually without asking for a new password.
| Your users are in | Do this |
|---|---|
| Auth0, Firebase, Cognito, Supabase, Clerk, Keycloak or Okta | Export them, import the file in the console. Missing passwords come over with lazy migration. |
| Your own database | Import a JSON or CSV file or use the Management API, or keep the database and connect it as an adapter with Import users on first login. |
| SSOfy | See Coming from SSOfy. |
#Import in the console
Import Users → Import a file. Pick the source, drop the export, and 9in converts and checks it in your browser, showing what will be imported, what is skipped and why, before anything is uploaded. Then:
- Keep user ids: the old ids become 9in user ids, so the
subyour apps already store doesn't change (auth0|60425dc4staysauth0|60425dc4). - Existing users: skip them, or update them with the file's data.
Social identities (Google, GitHub…) and original sign-up dates come along.
#Passwords
| Format | Typical source | |
|---|---|---|
bcrypt ($2a$, $2b$, $2y$) | Auth0 (hash export via support), Supabase, Clerk, most frameworks | Imported as is |
Argon2 (argon2id, argon2i, argon2d) | Keycloak, modern apps | Imported as is |
| PBKDF2 (SHA-1, SHA-256, SHA-512) | Keycloak, Django | Imported as is |
| Firebase scrypt | Firebase (with your project's hash parameters) | Imported as is |
Imported hashes are checked as they are and upgraded to argon2id the next time each user signs in.
#Lazy migration
Some providers don't export passwords (Cognito, Okta, Auth0 without the hash export). With lazy migration (Import Users → Lazy migration), when someone signs in and 9in has no password for them, 9in checks the password once with the old provider, then keeps it:
| Source | Needs |
|---|---|
| Auth0 | Tenant domain, a client ID and secret with the password grant, the database connection name |
| Amazon Cognito | Region and an app client with ALLOW_USER_PASSWORD_AUTH |
| Firebase | The Web API key |
| Supabase | The project URL and anon key |
| Any OIDC provider with the password grant (Okta, Keycloak, Ping…) | Issuer, client ID and secret |
Users who never come back can reset their password or sign in with a one-time code. Turn lazy migration off when you're done: no more calls to the old provider.
#Import with the API
POST /api/v1/users/import takes up to 500 users per request (50 if they carry plain-text passwords), with the management:users:write scope:
POST https://acme.9in.io/api/v1/users/import
Authorization: Bearer <token>
Content-Type: application/json
{
"on_conflict": "skip",
"source": "legacy",
"users": [
{
"id": "42",
"email": "jane@acme.com",
"emailVerified": true,
"name": "Jane Doe",
"passwordHash": "$2b$10$…",
"createdAt": 1680343200000,
"metadata": { "plan": "pro", "fields": { "company": "Acme" } },
"identities": [{ "connection": "social:google", "subject": "1098…" }]
}
]
}{ "results": [{ "index": 0, "ok": true, "status": "created", "id": "42" }] }Each record succeeds or fails on its own; results come back in order with created, updated, skipped (with a reason) or failed (with an error and details). Records match existing users by id, email, phone, username or social identity. Every imported user produces a user.imported entry in the activity log.
Identity connections are social:<provider>, enterprise:<connection id> or migration:<source>. Ids may contain letters, digits and | _ . : @ + - (up to 128 characters).
#Checklist
- Import into the sandbox first (
https://acme-sandbox.9in.io), try signing in as a few users, then import into production. - If ids change (you didn't keep them), map them in your app using the email or a value you put in
metadata. - Turn on the sign-in methods and social providers your users had.