# System for Cross-domain Identity Management (SCIM) (/docs/plugins/scim)

Provision users and groups from a directory into Better Auth.



The SCIM plugin adds an inbound System for Cross-domain Identity Management (SCIM) 2.0 service to Better Auth. A directory can use this service to create, update, deactivate, and delete users and groups in your application.

SCIM defines identity resources in [RFC 7643](https://www.rfc-editor.org/rfc/rfc7643) and the HTTP protocol in [RFC 7644](https://www.rfc-editor.org/rfc/rfc7644). Better Auth supports the operations and attributes listed in the [SCIM reference](/docs/plugins/scim/reference).

Each SCIM User links to a Better Auth User. Provisioning does not create a sign-in method or grant application access. Configure authentication separately, and use the optional identity and role callbacks when SCIM should affect your application.

## Installation [#installation]

<Steps>
  <Step>
    ### Install the plugin [#install-the-plugin]

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install @better-auth/scim
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @better-auth/scim
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @better-auth/scim
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @better-auth/scim
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Step>

  <Step>
    ### Enable database transactions [#enable-database-transactions]

    SCIM resource requests require a database adapter with native interactive transaction support. Enable transactions only when your database driver can run an interactive transaction callback.

    <Tabs items="[&#x22;Kysely&#x22;, &#x22;Drizzle&#x22;, &#x22;Prisma&#x22;]">
      <Tab value="Kysely">
        ```ts title="auth.ts"
        const auth = betterAuth({
          database: kyselyAdapter(db, {
            type: "sqlite",
            transaction: true,
          }),
        });
        ```
      </Tab>

      <Tab value="Drizzle">
        ```ts title="auth.ts"
        const auth = betterAuth({
          database: drizzleAdapter(db, {
            provider: "pg",
            transaction: true,
          }),
        });
        ```
      </Tab>

      <Tab value="Prisma">
        ```ts title="auth.ts"
        const auth = betterAuth({
          database: prismaAdapter(prisma, {
            provider: "postgresql",
            transaction: true,
          }),
        });
        ```
      </Tab>
    </Tabs>

    <Callout type="warn">
      Cloudflare D1 does not support the interactive transactions required by the SCIM plugin.
    </Callout>
  </Step>

  <Step>
    ### Configure a connection [#configure-a-connection]

    Create a high-entropy bearer token in your secret manager. Configure the same value in Better Auth and your directory.

    ```ts title="auth.ts"
    import { scim } from "@better-auth/scim";
    import { betterAuth } from "better-auth";

    const workforceToken = process.env.SCIM_WORKFORCE_TOKEN;

    if (!workforceToken) {
      throw new Error("SCIM_WORKFORCE_TOKEN is required");
    }

    export const auth = betterAuth({
      baseURL: "https://app.example.com/api/auth",
      plugins: [
        scim({
          connections: [
            {
              id: "workforce-acme",
              provisioningDomainId: "workspace-acme",
              credentials: [
                {
                  type: "bearer",
                  id: "workforce-primary",
                  token: workforceToken,
                },
              ],
            },
          ],
        }),
      ],
    });
    ```

    The connection ID owns the resources provisioned with this credential. The optional `provisioningDomainId` identifies the workspace, tenant, project, or other application boundary that receives lifecycle and role updates. It defaults to the connection ID.

    <Callout type="info">
      For runtime tenant onboarding and credential rotation, configure the optional [`managedConnections` catalog](/docs/plugins/scim/reference#managed-connection-catalog). It persists plugin-owned connection metadata and token digests while your server authorizes the customer-administrator UI. Applications with an existing catalog can instead resolve the connection atomically in `authentication.verifyBearerToken`.
    </Callout>
  </Step>

  <Step>
    ### Expose the SCIM methods [#expose-the-scim-methods]

    Your Better Auth route must forward `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. For Next.js, export every method from the handler.

    ```ts title="app/api/auth/[...all]/route.ts"
    import { auth } from "@/lib/auth";
    import { toNextJsHandler } from "better-auth/next-js";

    export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(auth);
    ```
  </Step>

  <Step>
    ### Create the database tables [#create-the-database-tables]

    Run the migration, or generate the schema if your application manages migrations.

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Configure your directory [#configure-your-directory]

Use the following values in your directory's SCIM connection settings:

| Setting        | Value                                                                                                             |
| -------------- | ----------------------------------------------------------------------------------------------------------------- |
| Base URL       | Your Better Auth base URL followed by `/scim/v2`, such as `https://app.example.com/api/auth/scim/v2`              |
| Authentication | Bearer token                                                                                                      |
| Token          | A static token from `connections[].credentials` or the one-time token returned by a managed create or rotate call |

The directory must send the token as `Authorization: Bearer <token>`. User and Group requests use `application/scim+json` or `application/json`. Discovery endpoints are public so the directory can inspect the supported resources and operations before provisioning.

## How provisioning maps to your application [#how-provisioning-maps-to-your-application]

The plugin keeps directory resources and application access separate:

* A connection owns an isolated set of SCIM Users, Groups, and direct Group memberships.
* A SCIM User links to one Better Auth User.
* A provisioning domain identifies where your application applies lifecycle and access changes.
* A SCIM Group grants no application permission unless you configure a role projection.

You can configure several connections. Their credentials remain isolated, while multiple connections may contribute to the same provisioning domain.

## Provision users [#provision-users]

When a directory creates a SCIM User, Better Auth creates a User by default, or links one when `identity.resolveUser` returns `link`. The directory can then update the profile, set `active`, or delete the SCIM resource. A SCIM User does not create an authentication account, so the user still needs SSO, a passkey, credentials, or another sign-in method.

By default, the SCIM source manages the Better Auth User's email and name. The plugin never links an incoming resource to an existing user by email.

### Link an existing Better Auth User [#link-an-existing-better-auth-user]

Use `identity.resolveUser` when your application already has a stable mapping from the directory subject to a Better Auth User. Prefer a directory-owned `externalId` when the directory keeps it immutable. Do not use an unverified email address as the link key.

```ts title="auth.ts"
scim({
  connections,
  identity: {
    async resolveUser(input, { database }) {
      const userId = input.resource.externalId
        ? await findUserIdByDirectorySubject(
            database,
            input.connectionId,
            input.resource.externalId,
          )
        : undefined;

      return userId
        ? { action: "link", userId, profile: "preserve" }
        : { action: "create" };
    },
  },
});
```

Choose the profile behavior when you link a user:

| Result                                            | Behavior                                                                       |
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
| `{ action: "create" }`                            | Creates a Better Auth User and lets the SCIM source manage its email and name. |
| `{ action: "link", userId, profile: "manage" }`   | Links an existing User and lets this source manage its email and name.         |
| `{ action: "link", userId, profile: "preserve" }` | Links an existing User without changing its Better Auth email or name.         |

Only one source can manage a Better Auth User's profile. Separate connections may link preserved sources to the same User, but one connection cannot create two SCIM resources for the same linked User.

## Authenticate provisioned users with SSO [#authenticate-provisioned-users-with-sso]

Use `acquireActiveSCIMUserLink` inside the SSO plugin's `resolveUser` callback when one SCIM connection controls who can sign in through a paired OIDC provider. The helper finds an active SCIM User by its exact connection ID and `externalId`, returning `{ scimUserId, userId }` for the active link. It returns `null` for missing, inactive, deleted, tombstoned, orphaned, or decommissioned links.

```ts title="auth.ts"
import { acquireActiveSCIMUserLink, scim } from "@better-auth/scim";
import { sso } from "@better-auth/sso";
import { betterAuth } from "better-auth";

const workforceProviderId = "acme-workforce-oidc";
const workforceConnectionId = "workforce-acme";

export const auth = betterAuth({
  plugins: [
    scim({ connections }),
    sso({
      defaultSSO: [
        {
          providerId: workforceProviderId,
          domain: "acme.example",
          oidcConfig: {
            issuer: "https://idp.acme.example",
            clientId: "acme-workforce-client-id",
            clientSecret: "acme-workforce-client-secret",
            pkce: true,
            discoveryEndpoint:
              "https://idp.acme.example/.well-known/openid-configuration",
          },
        },
      ],
      async resolveUser(input, context) {
        if (input.providerId !== workforceProviderId) {
          return { action: "continue" };
        }

        const link = await acquireActiveSCIMUserLink(
          {
            connectionId: workforceConnectionId,
            externalId: input.accountKey.accountId,
          },
          context,
        );

        return link
          ? {
              action: "link",
              userId: link.userId,
              profile: "preserve",
            }
          : {
              action: "reject",
              code: "SCIM_USER_NOT_ACTIVE",
            };
      },
    }),
  ],
});
```

Configure the directory to send the OIDC provider's immutable, case-exact subject as the SCIM User's `externalId`. The example uses the validated OIDC `sub` from `input.accountKey.accountId`. The helper never falls back to `userName`, email, another connection, or a deleted-resource tombstone.

Call the helper with the `context` supplied by `resolveUser`; its `database` is the same native transaction adapter used for account linking and session creation. The helper fences the link against concurrent subject changes, source deactivation or deletion, and connection decommissioning. A direct helper caller can retry its entire transaction after a conflict.

During SSO, a lifecycle conflict aborts the current authentication attempt and is returned as `SSO_USER_RESOLUTION_FAILED`; no Account or Session is created. SSO does not retry the callback automatically, and the original OIDC callback cannot be replayed. The user or client must start a fresh SSO authentication attempt, which re-evaluates the current SCIM state.

## Handle activation and deletion [#handle-activation-and-deletion]

Use `identity.reconcileUser` to apply the combined SCIM lifecycle state to your application. The callback receives every source linked to the Better Auth User and an `active` value that is `true` while at least one source remains active.

```ts title="auth.ts"
scim({
  connections,
  identity: {
    async reconcileUser(state, { database }) {
      await setDirectoryAccessState(database, state.userId, state.active);
    },
  },
});
```

The callback runs in the same database transaction as the SCIM change. Make it idempotent and use the supplied `database` transaction for writes. Throwing from the callback rejects the request and rolls back the SCIM change.

When the final active source is deactivated or deleted, Better Auth deletes the User's sessions. Reactivating a source updates the lifecycle state but does not create a session.

<Callout type="warn">
  Deleting sessions does not prevent a user from signing in again. Persist the disabled state in `identity.reconcileUser`, then enforce it in your authentication or authorization policy when deactivation must block future sign-in.
</Callout>

Deleting a SCIM resource preserves its Better Auth User. If the resource has an `externalId`, recreating the same `externalId` through the same connection links the new SCIM resource to that User. Without an `externalId`, the next create follows your resolver or creates another Better Auth User.

## Next steps [#next-steps]

<Cards>
  <Card href="/docs/plugins/scim/groups-and-roles" title="Groups and custom roles">
    Provision direct memberships and map directory Groups to application roles.
  </Card>

  <Card href="/docs/plugins/scim/reference" title="SCIM reference">
    Review options, endpoints, attributes, filters, PATCH paths, limits, and schema.
  </Card>
</Cards>

