# Migrate from Clerk (/docs/guides/migration/from-clerk)

> Move users, organizations, memberships, organization roles and permissions, and metadata from Clerk to Better IAM with identities.createMany and configuration documents, recreate social and enterprise SSO, replace Clerk in a Next.js app, and cut over.



This guide moves an application from Clerk to Better IAM. Clerk keeps signing people in while you import, and you
switch one organization (or the whole application, if it has no organizations) at a time. You will:

* export users, organizations, memberships, and organization roles from Clerk;
* declare Clerk's custom permissions as actions and recreate organization roles in every tenant;
* create one Better IAM tenant per Clerk organization, plus one for people without an organization, and import
  members with their roles and selected metadata;
* recreate social and enterprise SSO connections;
* replace Clerk's middleware, `auth()` calls, and components in a Next.js app;
* cut over and know how to roll back.

The general plan behind these steps is in [Migration guides](/docs/guides/migration).

## How Clerk concepts map [#how-clerk-concepts-map]

| Clerk                                                   | Better IAM                                                                 | Notes                                                                                                                                                                         |
| ------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Application and its instances (development, production) | One deployment per environment                                             | Each has its own database and secret.                                                                                                                                         |
| User                                                    | Identity                                        | Keep the Clerk user ID as an attribute (`clerkId` below).                                                                                                                     |
| Primary email address                                   | The identity's `email`                                                     | One address per identity. Secondary addresses are not kept.                                                                                                                   |
| First and last name                                     | `name`                                                                     | One display name.                                                                                                                                                             |
| Username                                                | An attribute, if you display it                                            | Sign-in is by email; usernames are not a sign-in identifier.                                                                                                                  |
| Phone number                                            | Not imported                                                               | A phone number is set only when the person verifies it with `auth.startPhoneVerification`. Users with only a phone number cannot be imported: every identity needs an email.  |
| `publicMetadata`, `unsafeMetadata`                      | Identity attributes                                                        | Flat `string`, `number`, or `boolean` values declared in `permissions.identityAttributes`.                                                                                    |
| `privateMetadata`                                       | Your own database                                                          | Attributes are returned to the person in their session, so they are not private.                                                                                              |
| Organization                                            | Tenant (type `organization`)                      | Each tenant has its own people, roles, sign-in policy, and connections.                                                                                                       |
| Organization membership                                 | An identity in that tenant                                                 | A person in two organizations gets two identities with the same email.                                                                                                        |
| Active organization                                     | The tenant of the current session                                          | A session belongs to one identity in one tenant. Switching organizations means switching accounts ([account linking](/docs/concepts/tenants-and-identities#account-linking)). |
| Personal account (no organization)                      | An identity in one tenant you create for these people                      |                                                                                                                                                                               |
| Organization role (`org:admin`, `org:member`, custom)   | Role, per tenant                                    | Apply the same role document to every tenant.                                                                                                                                 |
| Custom permission (`org:invoices:create`)               | Action declared in `permissions.resourceTypes`    | Rename to your resource types' style (`invoices:create`).                                                                                                                     |
| Organization invitation                                 | `identities.invite`                                                        |                                                                                                                                                                               |
| Verified organization domain                            | `domains.add` and `domains.verify`                                         | `domains.discover` routes sign-in by email domain.                                                                                                                            |
| Session token and JWT templates                         | [Session tokens](/docs/authentication/tokens)                              | Database sessions by default; JWT sessions for services that verify locally.                                                                                                  |
| Social connection                                       | `createOAuthLogin` connection                                              | Fixed in the deployment configuration; each connection serves one tenant.                                                                                                     |
| Enterprise connection (SAML, OIDC)                      | SAML connection or OIDC connection                                         |                                                                                                                                                                               |
| Webhooks                                                | [Webhooks](/docs/concepts/events/webhooks)                                 | Audit events, signed and retried.                                                                                                                                             |
| Banned or locked user                                   | `identities.setStatus` (`disabled`), `identities.unlock`                   |                                                                                                                                                                               |
| User impersonation                                      | [Impersonation](/docs/authentication/impersonation)                        | Off by default per tenant.                                                                                                                                                    |
| MFA (authenticator app, SMS, backup codes), passkeys    | [MFA](/docs/authentication/mfa), [passkeys](/docs/authentication/passkeys) | People enroll again.                                                                                                                                                          |
| Prebuilt components (`<SignIn />`, `<UserButton />`)    | Your own components                                                        | Better IAM has no hosted or prebuilt sign-in UI.                                                                                                                              |

> **What does not move.** 
  Passwords (no Better IAM API accepts a password digest, whatever your Clerk export contains), MFA enrollments,
  passkeys, phone numbers, and active sessions. People who used a password sign in once with a magic link or a reset
  link. Verified email status and links to Google or GitHub accounts are carried over by the import, so social users
  keep signing in through their provider.

## Prerequisites [#prerequisites]

* A Better IAM deployment with a migrated database and a bootstrapped root administrator
  ([Quickstart](/docs/quickstart)), configured with `authentication.sendEmail`.
* A Clerk secret key for the Backend API of the instance you are leaving.
* For a Next.js app, the [Next.js integration](/docs/integrations/nextjs) installed next to Clerk, so both can run
  during the dual run.
* A root administrator session for the scripts:

```bash
npx better-iam login --config better-iam.config.mjs --tenant "$ROOT_TENANT_ID" --email root@example.com
export BETTER_IAM_TOKEN=$(npx better-iam token --config better-iam.config.mjs)
```

      ### Export from Clerk [#export-from-clerk]

    Use Clerk's Backend API to list everything page by page and save it as files. Write user objects exactly as the API
    returns them; the import script reads the fields named here.

    | File                              | Contents                                                                                                                                                                                                                                                               |
    | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `clerk/users.ndjson`              | One Clerk user object per line. The script reads `id`, `first_name`, `last_name`, `primary_email_address_id`, `email_addresses` (`id`, `email_address`, `verification.status`), `external_accounts` (`provider`, `provider_user_id`), `public_metadata`, and `banned`. |
    | `clerk/organizations.json`        | `[{ id, name, ownerUserId }]`. Pick one owner per organization: an admin who should control it in Better IAM.                                                                                                                                                          |
    | `clerk/memberships-<org_id>.json` | `[{ userId, role }]`: each membership's user ID and role key, such as `org:admin`.                                                                                                                                                                                     |
    | `clerk/roles.json`                | `[{ key, name, permissions: [permission_key] }]` for every organization role.                                                                                                                                                                                          |
    | `clerk/sso.json`                  | Which organizations use enterprise SSO, with their email domains and connection type.                                                                                                                                                                                  |

    An external account's `provider_user_id` is the person's ID at that provider: Google's `sub` for `oauth_google`, the
    numeric user ID for `oauth_github`. The import links those accounts to your new connections. People who sign in
    through a customer's enterprise SSO are left out by default, so their first SSO sign-in creates their account; list
    their email domains in `ENTERPRISE_DOMAINS` in the import script.
  
      ### Configure Better IAM [#configure-better-iam]

    Declare attributes for the Clerk user ID and any public metadata you keep, and turn custom permissions into
    actions. Turn on magic links so imported people can sign in without a password.

    ```ts title="iam.ts"
    import { betterIam } from 'better-iam';
    import { postgresAdapter } from 'better-iam/adapter-postgres';
    import { sendEmail } from './mail';

    export const iam = betterIam({
      database: postgresAdapter({ connectionString: process.env.DATABASE_URL! }),
      secret: process.env.BETTER_IAM_SECRET!,
      baseURL: process.env.BETTER_IAM_BASE_URL!,
      authentication: {
        sendEmail,
        passwordlessEmail: true,
      },
      onboarding: { mode: 'linked' }, // people in several organizations can switch between their accounts
      permissions: {
        identityAttributes: { clerkId: 'string', plan: 'string' },
        resourceTypes: {
          invoice: { actions: ['invoices:read', 'invoices:create'] },
        },
      },
    });
    ```

    `plan` stands for a flat value you keep from `publicMetadata`. Nested metadata stays in your own database, keyed by
    the new identity ID from the import's ID map.

    ```bash
    npx better-iam migrate --config better-iam.config.mjs
    npx better-iam store-export --config better-iam.config.mjs --output before-clerk-import.jsonl
    ```
  
      ### Create tenants [#create-tenants]

    Create one tenant per Clerk organization, and one more for people who do not belong to any organization, owned by
    someone on your team (`PERSONAL_OWNER_USER_ID` is their Clerk user ID). With `activate: true`, which your root
    administrator session may pass, each tenant is `active` at once with its owner's account, so members can be imported
    right away. No invitation is sent: owners sign in with a magic link, a reset link, or a provider you link to them.

    ```ts title="scripts/clerk-create-tenants.ts"
    import { createReadStream } from 'node:fs';
    import { readFile, writeFile } from 'node:fs/promises';
    import { createInterface } from 'node:readline';
    import { iam } from '../iam';

    type ClerkOrganization = { id: string; name: string; ownerUserId: string };
    type ClerkUser = {
      id: string;
      first_name: string | null;
      last_name: string | null;
      primary_email_address_id: string | null;
      email_addresses: Array<{ id: string; email_address: string; verification?: { status: string } | null }>;
    };

    const credential = { token: process.env.BETTER_IAM_TOKEN! };
    const rootTenantId = process.env.ROOT_TENANT_ID!;

    const owners = new Map<string, { email: string; verified: boolean; name: string }>();
    for await (const line of createInterface({ input: createReadStream('clerk/users.ndjson'), crlfDelay: Infinity })) {
      if (!line.trim()) continue;
      const user = JSON.parse(line) as ClerkUser;
      const primary = user.email_addresses.find((address) => address.id === user.primary_email_address_id);
      if (!primary) continue;
      const name = [user.first_name, user.last_name].filter(Boolean).join(' ') || primary.email_address;
      owners.set(user.id, { email: primary.email_address, verified: primary.verification?.status === 'verified', name });
    }

    const organizations = JSON.parse(await readFile('clerk/organizations.json', 'utf8')) as ClerkOrganization[];
    organizations.push({ id: 'personal', name: 'Personal accounts', ownerUserId: process.env.PERSONAL_OWNER_USER_ID! });

    const tenantMap: Record<string, string> = {};
    for (const organization of organizations) {
      const owner = owners.get(organization.ownerUserId);
      if (!owner) throw new Error(`No email for the owner of ${organization.name}`);
      const { tenant } = await iam.api.tenants.create(credential, {
        parentId: rootTenantId,
        type: 'organization',
        name: organization.name,
        ownerEmail: owner.email,
        activate: true, // active now; the owner's account is created without an invitation
        ownerName: owner.name,
        ownerEmailVerified: owner.verified,
      });
      tenantMap[organization.id] = tenant.id;
    }
    await writeFile('clerk/tenant-map.json', JSON.stringify(tenantMap, null, 2));
    ```

    ```bash
    npx tsx scripts/clerk-create-tenants.ts
    ```

    `tenants.create` needs recent authentication, so run it within five minutes of `better-iam login`. Only a root
    principal may pass `activate`; without it, the owner receives an emailed invitation and the tenant stays `pending`
    until they accept it. An owner with accounts in several tenants links them afterwards with `links.create`
    ([account linking](/docs/concepts/tenants-and-identities#account-linking)).
  
      ### Recreate organization roles [#recreate-organization-roles]

    Build one configuration document from `clerk/roles.json` and apply it to each tenant. Role names in Better
    IAM are display names, so `org:admin` becomes `Admin`; keep the mapping for the import.

    ```ts title="scripts/clerk-roles.ts"
    import { readFile } from 'node:fs/promises';
    import { iam } from '../iam';

    type ClerkRole = { key: string; name: string; permissions: string[] };

    // Clerk permission key -> Better IAM action.
    const actionFor: Record<string, string> = {
      'org:invoices:read': 'invoices:read',
      'org:invoices:create': 'invoices:create',
    };

    const credential = { token: process.env.BETTER_IAM_TOKEN! };

    export async function applyClerkRoles(tenantId: string): Promise<Map<string, string>> {
      const roles = JSON.parse(await readFile('clerk/roles.json', 'utf8')) as ClerkRole[];
      await iam.api.config.apply(credential, {
        tenantId,
        config: {
          version: 1,
          roles: roles.map((role) => {
            // Clerk's system permissions (org:sys_*) correspond to Better IAM's own iam:* actions (see below).
            const permissions = role.permissions
              .filter((key) => !key.startsWith('org:sys_'))
              .map((key) => {
                const action = actionFor[key];
                if (!action) throw new Error(`No action mapped for Clerk permission ${key}`);
                return action;
              });
            // A permissions list must name at least one action; a role without any grants nothing yet.
            return permissions.length ? { name: role.name, permissions } : { name: role.name };
          }),
        },
      });
      const byName = new Map((await iam.api.roles.list(credential, { tenantId })).map((role) => [role.name, role.id]));
      // Clerk role key -> Better IAM role ID.
      return new Map(roles.flatMap((role) => (byName.has(role.name) ? [[role.key, byName.get(role.name)!]] : [])));
    }
    ```

    Clerk's administrative abilities (managing members, domains, and settings) correspond to Better IAM's own `iam:*`
    actions. The tenant's owner holds all of them through the protected Owner role. To let other admins manage members,
    create a role whose `document` allows the `iam:*` actions they need, as in
    [Roles and bindings](/docs/authorization/roles).
  
      ### Import members [#import-members]

    ```ts title="scripts/clerk-import-members.ts"
    import { createReadStream } from 'node:fs';
    import { readFile, writeFile } from 'node:fs/promises';
    import { createInterface } from 'node:readline';
    import { iam } from '../iam';
    import { applyClerkRoles } from './clerk-roles';

    type ClerkUser = {
      id: string;
      first_name: string | null;
      last_name: string | null;
      primary_email_address_id: string | null;
      email_addresses: Array<{ id: string; email_address: string; verification?: { status: string } | null }>;
      external_accounts?: Array<{ provider: string; provider_user_id: string }>;
      public_metadata: Record<string, unknown>;
      banned: boolean;
    };
    type Membership = { userId: string; role: string };
    type Link = { providerId: string; issuer: string };

    // Email domains of customers who sign in through their own SSO: never imported.
    const ENTERPRISE_DOMAINS = new Set(['acme.com']);

    // Clerk external account provider -> the OAuth connection that replaces it, per Clerk organization ('personal' for
    // people without one). providerId is the connection's id in sso.ts; issuer is the provider's `iss`, exactly.
    const OAUTH_CONNECTIONS: Record<string, Record<string, Link>> = {
      personal: {
        oauth_google: { providerId: 'personal-google', issuer: 'https://accounts.google.com' },
      },
    };

    const credential = { token: process.env.BETTER_IAM_TOKEN! };

    const users = new Map<string, ClerkUser>();
    for await (const line of createInterface({ input: createReadStream('clerk/users.ndjson'), crlfDelay: Infinity })) {
      if (line.trim()) {
        const user = JSON.parse(line) as ClerkUser;
        users.set(user.id, user);
      }
    }

    function primaryAddress(user: ClerkUser) {
      return user.email_addresses.find((address) => address.id === user.primary_email_address_id);
    }

    /** One link per Better IAM connection, from the user's Clerk external accounts. */
    function providerLinks(user: ClerkUser, clerkOrgId: string) {
      const targets = OAUTH_CONNECTIONS[clerkOrgId] ?? {};
      const links = new Map<string, Link & { subject: string }>();
      for (const account of user.external_accounts ?? []) {
        const target = targets[account.provider];
        if (target) links.set(target.providerId, { ...target, subject: account.provider_user_id });
      }
      return [...links.values()];
    }

    async function existingEmails(tenantId: string): Promise<Set<string>> {
      const emails = new Set<string>();
      for (let offset = 0; ; offset += 1000) {
        const page = await iam.api.identities.list(credential, { tenantId, kind: 'user', limit: 1000, offset });
        for (const identity of page) if (identity.email) emails.add(identity.email);
        if (page.length < 1000) return emails;
      }
    }

    async function importTenant(clerkOrgId: string, tenantId: string, memberships: Membership[]): Promise<void> {
      const roleIds = await applyClerkRoles(tenantId);
      const seen = await existingEmails(tenantId);

      const rows = memberships.flatMap(({ userId, role }) => {
        const user = users.get(userId);
        const address = user && primaryAddress(user);
        const email = address?.email_address.trim().toLowerCase();
        if (!user || !address || !email || user.banned) return []; // no email, or banned: not imported
        if (ENTERPRISE_DOMAINS.has(email.split('@')[1]!)) return []; // arrives through SSO
        if (seen.has(email)) return []; // already imported, or the owner
        seen.add(email);
        const name = [user.first_name, user.last_name].filter(Boolean).join(' ') || email;
        const plan = user.public_metadata.plan;
        return [{
          user,
          entry: {
            email,
            name,
            emailVerified: address.verification?.status === 'verified',
            providers: providerLinks(user, clerkOrgId),
            attributes: { clerkId: user.id, ...(typeof plan === 'string' ? { plan } : {}) },
            roleIds: roleIds.has(role) ? [roleIds.get(role)!] : [],
          },
        }];
      });

      const idMap: Record<string, string> = {};
      for (let start = 0; start < rows.length; start += 100) {
        const batch = rows.slice(start, start + 100);
        const { identities } = await iam.api.identities.createMany(credential, {
          tenantId,
          identities: batch.map((row) => row.entry),
        });
        batch.forEach((row, i) => (idMap[row.user.id] = identities[i]!.id));
      }
      await writeFile(`clerk/id-map-${clerkOrgId}.json`, JSON.stringify(idMap, null, 2));
      console.log(`${clerkOrgId}: imported ${rows.length}`);
    }

    const tenantMap = JSON.parse(await readFile('clerk/tenant-map.json', 'utf8')) as Record<string, string>;
    const inAnOrganization = new Set<string>();
    for (const [clerkOrgId, tenantId] of Object.entries(tenantMap)) {
      if (clerkOrgId === 'personal') continue;
      const memberships = JSON.parse(await readFile(`clerk/memberships-${clerkOrgId}.json`, 'utf8')) as Membership[];
      memberships.forEach((membership) => inAnOrganization.add(membership.userId));
      await importTenant(clerkOrgId, tenantId, memberships);
    }

    // Everyone without an organization goes into the personal tenant, with no role.
    const personal = [...users.keys()].filter((id) => !inAnOrganization.has(id)).map((userId) => ({ userId, role: '' }));
    await importTenant('personal', tenantMap.personal!, personal);
    ```

    ```bash
    npx tsx scripts/clerk-import-members.ts
    ```

    * Run it again during the dual run to pick up new Clerk sign-ups: existing addresses are skipped.
    * `emailVerified` is set only for addresses Clerk verified. Each Google account in `OAUTH_CONNECTIONS` becomes a
      link, so that person's first Google sign-in finds their imported account. Add an entry for every provider and
      tenant you recreate (`oauth_github` with `https://github.com`, for example); accounts without one get no link, and
      those people sign in with a magic link.
    * The owner already exists, so the script skips them. If they sign in with Google, link it with
      `iam.api.identities.linkProvider(credential, { tenantId, identityId, providerId, issuer, subject })`, which needs a
      sign-in from the last five minutes.
    * Each batch of 100 is one transaction; a failed batch creates nobody, so fix the cause and run the script again.
    * The `id-map-*.json` files map each Clerk user ID to the new identity ID. Use them to rewrite user IDs stored in
      your own tables, and to move nested or private metadata into your database.
    * Banned users are skipped. If you need them for history, import them and call `identities.setStatus` with
      `status: 'disabled'`.
  
      ### Recreate social and enterprise sign-in [#recreate-social-and-enterprise-sign-in]

    Social sign-in becomes a `createOAuthLogin` connection per provider and tenant. If Clerk used your own OAuth
    credentials, reuse the client ID and secret and add the new `redirectUri` at the provider; Clerk's shared
    development credentials cannot be reused.

    ```ts title="sso.ts"
    import { createOAuthLogin } from 'better-iam/oauth';
    import { iam } from './iam';

    export const login = createOAuthLogin({
      ...iam.protocolHost,
      trustedOrigins: ['https://app.example.com'],
      connections: [
        {
          id: 'personal-google',
          tenantId: process.env.PERSONAL_TENANT_ID!,
          kind: 'google',
          clientId: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
          redirectUri: 'https://app.example.com/oauth/personal-google/callback',
        },
        {
          id: 'acme-entra',
          tenantId: process.env.ACME_TENANT_ID!,
          kind: 'microsoft',
          microsoftTenant: process.env.ACME_ENTRA_TENANT_ID!,
          clientId: process.env.ACME_ENTRA_CLIENT_ID!,
          clientSecret: process.env.ACME_ENTRA_CLIENT_SECRET!,
          redirectUri: 'https://app.example.com/oauth/acme-entra/callback',
        },
      ],
    });
    iam.useProtocol(login);
    ```

    The connection `id` is the `providerId` in the import script's `OAUTH_CONNECTIONS`, so people who signed in to
    Clerk with Google sign in to their imported account, with its role, on their first Google sign-in. Someone who was
    not in the export is created on their first sign-in with a verified email.

    For a customer on Clerk's enterprise SAML, create a SAML connection in their tenant from their IdP's metadata with
    `saml.createConnection`, including `trustedEmailDomains`, and have their IT team add a second application in their
    IdP with the returned `entityId` and `acsUrl`. The steps are in [SAML](/docs/federation/saml#tenant-managed-connections)
    and [SAML single sign-on with Okta](/docs/guides/enterprise-sso-okta). Their people's first SSO sign-in creates
    their accounts, because the import skipped their domains. To keep their Clerk roles instead, remove the domain from
    `ENTERPRISE_DOMAINS` once the connection exists and give each person a `providers` entry for it: the connection's
    `providerId` and `idpIssuer` (from `saml.createConnection` or `saml.getConnection`) and the NameID their IdP sends
    for them. For an OIDC connection such as `acme-entra`, use its `id`, its issuer, and the person's `sub`.
  
      ### Replace Clerk in a Next.js app [#replace-clerk-in-a-nextjs-app]

    | Clerk                                             | Better IAM                                                                                                          |
    | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
    | `clerkMiddleware()` in `middleware.ts`            | `createIamMiddleware` from `@better-iam/next/edge`                                                                  |
    | `auth()` and `currentUser()` in server components | `iamNext.getSession()`, which returns `{ identity, session }` or `null`                                             |
    | `auth().protect()`, `has({ permission })`         | `iamNext.page(render, spec)` and `iamNext.route(handler, spec)` guards ([Guards](/docs/integrations/nextjs/guards)) |
    | `useUser()`, `useAuth()` in client components     | `useSession()` from `@better-iam/react`                                                                             |
    | `<SignIn />`, `<SignUp />`                        | Your own forms, with `iamNext.authActions()` server actions                                                         |
    | `<OrganizationSwitcher />`                        | `links.list` and `links.switch` between a person's linked accounts                                                  |
    | `clerkClient.users.*`                             | `iam.api.identities.*` with an administrator credential                                                             |

    ```ts title="middleware.ts"
    import { NextResponse } from 'next/server';
    import { createIamMiddleware } from '@better-iam/next/edge';

    export const middleware = createIamMiddleware({
      loginPath: '/login',
      publicPaths: ['/', '/pricing', '/login'],
      next: (init) => NextResponse.next(init),
    });
    export const config = { matcher: ['/((?!_next|favicon.ico).*)'] };
    ```

    ```tsx title="app/dashboard/page.tsx"
    import { iamNext } from '@/lib/iam-next';

    export default async function Dashboard() {
      const current = await iamNext.getSession();
      if (!current) return <a href="/login">Sign in</a>;
      return <h1>Welcome, {current.identity.name}</h1>;
    }
    ```

    `lib/iam-next.ts` creates `iamNext` with `createIamNext(iam, { loginPath: '/login' })`; see
    [Next.js](/docs/integrations/nextjs#create-the-nextjs-helpers). Wherever your tables store Clerk user IDs, rewrite
    them with the ID maps, or look people up by the `clerkId` attribute during the transition.
  
      ### Cut over [#cut-over]

    Switch one tenant at a time (or the whole application at once, if it has only the personal tenant):

    1. Check counts and sample people ([Verify it works](#verify-it-works)).
    2. Route that organization's sign-in to Better IAM. Clerk sessions already open keep working until they end.
    3. Tell people how to sign in the first time: a magic link, their company's SSO, or Google as before.
    4. For SSO customers, restrict the tenant with `tenants.setAuthPolicy` and `allowedMethods: ['federated']` once the
       connection works and the owner's SSO account is linked (`identities.linkProvider`); people's first SSO sign-in
       creates or finds their accounts without any other method.

    Keep the Clerk instance unchanged until the rollback window ends. To roll a tenant back, route it to Clerk again;
    accounts created and passwords set in Better IAM after the switch do not exist in Clerk. See
    [Roll back](/docs/guides/migration#roll-back).
  
## Verify it works [#verify-it-works]

```ts title="scripts/clerk-verify.ts"
import { readFile } from 'node:fs/promises';
import { iam } from '../iam';

const credential = { token: process.env.BETTER_IAM_TOKEN! };
const tenantMap = JSON.parse(await readFile('clerk/tenant-map.json', 'utf8')) as Record<string, string>;

for (const [clerkOrgId, tenantId] of Object.entries(tenantMap)) {
  const usage = await iam.api.tenants.usage(credential, { tenantId });
  const idMap = JSON.parse(await readFile(`clerk/id-map-${clerkOrgId}.json`, 'utf8').catch(() => '{}'));
  console.log(clerkOrgId, 'identities', usage.identities, 'imported', Object.keys(idMap).length, 'roles', usage.roles);
}
```

* Each tenant's `identities` equals the imported members plus its owner and anyone who already arrived through SSO.
* An imported person requests a magic link (`auth.startPasswordless`), signs in, and sees their organization's data.
* `policies.effectiveActions` for a sampled admin and member lists the actions their Clerk role allowed
  ([What can a person do](/docs/authorization/reviews#what-can-a-person-do)).

## Troubleshooting [#troubleshooting]

  #### createMany fails with INVALID_INPUT for an attribute

    An attribute is not declared in `permissions.identityAttributes`, or its value has the wrong type (attributes are
    flat strings, numbers, or booleans). Declare it, or keep that metadata in your own database. See
    [INVALID\_INPUT](/docs/reference/errors#invalid_input).
  
  #### tenants.create fails with ACCESS_DENIED

    Only a root principal may pass `activate: true`. Sign in to the CLI as the root administrator and export that
    token. See [ACCESS\_DENIED](/docs/reference/errors#access_denied).
  
  #### createMany fails with TENANT_INACTIVE

    The tenant was created without `activate: true` and its owner has not accepted the invitation yet. See
    [TENANT\_INACTIVE](/docs/reference/errors#tenant_inactive).
  
  #### config.apply fails with INVALID_ACTION

    A role names an action not declared in `permissions.resourceTypes`. Add it, or fix `actionFor`. See
    [INVALID\_ACTION](/docs/reference/errors#invalid_action).
  
  #### A person in two organizations sees only one

    In Better IAM they have one account per organization. Turn on `onboarding: { mode: 'linked' }` and let them link
    the accounts with `links.create`; then `links.switch` moves between them. See
    [Account linking](/docs/concepts/tenants-and-identities#account-linking).
  
  #### Google sign-in fails with ACCOUNT_LINK_REQUIRED

    The person was imported without a link to this connection: their Clerk user had no Google external account, or
    `OAUTH_CONNECTIONS` had no entry for their tenant when they were imported. Add the link with
    `identities.linkProvider` (Google's `sub` as the subject). See
    [ACCOUNT\_LINK\_REQUIRED](/docs/reference/errors#account_link_required).
  
  #### createMany fails with ACCOUNT_LINK_CONFLICT

    Two Clerk users in the same tenant list the same external account, or a person already has a link through that
    connection. Fix the export, or find the existing link with `identities.listProviders`. See
    [ACCOUNT\_LINK\_CONFLICT](/docs/reference/errors#account_link_conflict).
  
## Next steps [#next-steps]

  - [Migration guides](/docs/guides/migration): The general plan: what carries over, dual run, and rollback.

  - [Next.js](/docs/integrations/nextjs): Sessions, guards, middleware, and server actions.

  - [Tenants and identities](/docs/concepts/tenants-and-identities): Why an identity belongs to one tenant, and how linking works.

  - [Magic links and codes](/docs/authentication/passwordless): The first sign-in for every imported person.
