Documentation menu

SCIM provisioning

How Atrium pushes users and teams into downstream services like Sentry, Slack, and GitHub Enterprise — and when not to use SCIM.

When someone joins your team in Zitadel, they need accounts in every tool the team uses. Doing that by hand doesn't scale. SCIM is the industry-standard protocol that lets an identity provider push users and groups into downstream services automatically.

Atrium acts as the SCIM client: when an admin grants a user access to an app (directly or by adding them to a group), Atrium provisions that user — and the right team membership — to the app's SCIM endpoint. When the grant is removed, Atrium deactivates the account.

How it fits together

Three players. Zitadel owns identity. Atrium owns the directory + the policy ("which Atrium group grants which downstream role"). The downstream owns its own user list — Atrium just keeps it in sync.

What is SCIM, really?

SCIM (System for Cross-domain Identity Management, RFC 7644) is a REST-based protocol with two main endpoints on every compliant downstream:

  • /Users — create, update, deactivate user accounts
  • /Groups — create groups (often called Teams in product UI), add or remove members

Every SCIM request uses a standard JSON resource shape and a bearer token for authentication. Most enterprise SaaS products that want to be "IdP-friendly" expose a SCIM endpoint — Sentry, Slack Enterprise, GitHub Enterprise, Okta, Datadog, and many more.

The mental model

Atrium thinks of SCIM provisioning as mirroring its own access state onto the downstream:

Atrium groups become downstream Teams. Atrium users with a grant on the SCIM-connected app become downstream accounts. Membership lines up. When you change one side, the reconciler converges the other.

The reconciliation loop

Reconciliation runs on every "Sync now" click and on the configured cron. It's idempotent — running it ten times produces the same result as running it once.

Each step writes a line to the run log so an admin can audit exactly what changed. Failures don't abort the loop — Atrium records them per-row, completes the rest, and surfaces a partial-success status so a transient network error doesn't block ten unrelated provisions.

When the dry-run / apply distinction matters

Reconciliation has two modes:

  • Dry run — compute what would change; write nothing. Used for the "Detect drift" button. Safe to fire any time, on any schedule.
  • Apply — same diff, then actually make the SCIM calls. Used by the "Sync now" button and by the scheduled cron once an admin sets ATRIUM_RECONCILE_CRON=apply on the deployment.

In practice, admins start a connection in dry-run-only, watch the diff for a few cycles to confirm it's sensible, then flip the cron to apply. There's no point automating provisioning until you trust the diff.

SCIM vs Atrium Actions — the decision

Not every downstream speaks SCIM. The rule:

DownstreamToolWhy
Sentry SaaS (Business+ plan with SCIM enabled), Slack Enterprise, GitHub Enterprise, Okta, DatadogSCIMThey publish a SCIM v2 endpoint; the standard protocol does the work.
Sentry self-hostedActionSelf-hosted Sentry doesn't enable SCIM by default — the /scim/v2/ endpoint returns 403. Use the regular REST API instead (template: Sync grants to Sentry).
Dokploy, Airtable, internal apps with RESTActionTheir data model isn't user-and-group-shaped (Dokploy has projects + applications) — write a script that calls their API directly when a grant changes.
Anything with no APIManualAdd an instruction in the app's "How to request access" so admins know to provision by hand.

Surprise to know: plenty of products that "support SCIM" only do so on their highest plan or behind a feature flag. Probe the SCIM endpoint with a simple curl before assuming — if you get 403 with a valid admin token, SCIM isn't enabled and an Action against their REST API is the working path. Templates for Sentry, Dokploy, and SCIM-fanout are all in /admin/actions → Templates.

Connecting a SCIM downstream

The form on /admin/groupsSCIM asks for four things:

  1. App — which Atrium app this connection serves. The reconciler reads grants on this app's Zitadel project.
  2. Endpoint URL — the downstream's SCIM v2 base URL, e.g. https://sentry.io/api/0/organizations/ommax/scim/v2/.
  3. Bearer token — issued by the downstream when an admin enables SCIM in their settings. Stored encrypted at rest in Atrium.
  4. Cron schedule — optional. When set, the connection reconciles automatically on that schedule.

Auto-deactivate removed users is a separate toggle: when on, users who no longer have a grant get DELETEd on the downstream. Off by default — many teams want to keep the account but remove the access, so the toggle is opt-in.

Where it shows up

After a connection is wired:

  • /admin/groups → SCIM lists every connection with its last-run status, run-time, and any errors.
  • The connection row → "Sync now" triggers an apply run on demand.
  • The connection row → "View log" shows the JSON diff from the last run.

Drift between Atrium's expected state and the downstream's actual state is recorded the same way as the diff would be applied — so dry-run output is the source of truth for "what would change if I clicked Sync now".

Failure modes worth knowing about

  • Bearer token expired or revoked — every SCIM call returns 401. The connection's last-run status flips to ERROR; the next dry run surfaces the same error so admins see it before clicking Sync.
  • Endpoint URL changed on the downstream side — SCIM endpoints sometimes get versioned (e.g. /scim/v2//scim/v3/). Edit the connection, paste the new URL, save.
  • Schema mismatch — the downstream returned 422 because Atrium sent a field it didn't recognise. Surface the response body in the run log; usually means the downstream's SCIM dialect needs a custom Action instead.
  • Network blip — partial success. The reconciler completes the rest of the diff and logs the failed rows for retry on the next run.