Users

Importing users

Move users from Auth0, Firebase, Cognito, Supabase, Clerk, Keycloak, Okta or your own database, with or without their passwords.

View .md llms.txt

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 inDo this
Auth0, Firebase, Cognito, Supabase, Clerk, Keycloak or OktaExport them, import the file in the console. Missing passwords come over with lazy migration.
Your own databaseImport 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.
SSOfySee 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 sub your apps already store doesn't change (auth0|60425dc4 stays auth0|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

FormatTypical source
bcrypt ($2a$, $2b$, $2y$)Auth0 (hash export via support), Supabase, Clerk, most frameworksImported as is
Argon2 (argon2id, argon2i, argon2d)Keycloak, modern appsImported as is
PBKDF2 (SHA-1, SHA-256, SHA-512)Keycloak, DjangoImported as is
Firebase scryptFirebase (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:

SourceNeeds
Auth0Tenant domain, a client ID and secret with the password grant, the database connection name
Amazon CognitoRegion and an app client with ALLOW_USER_PASSWORD_AUTH
FirebaseThe Web API key
SupabaseThe 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:

http
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…" }]
    }
  ]
}
json
{ "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

  1. Import into the sandbox first (https://acme-sandbox.9in.io), try signing in as a few users, then import into production.
  2. If ids change (you didn't keep them), map them in your app using the email or a value you put in metadata.
  3. Turn on the sign-in methods and social providers your users had.