Skip to content

How-to

SCIM provisioning basics

Provisioning is easy to demonstrate and easy to get subtly wrong. A handler that creates users correctly and deactivates them unreliably will pass every demo and fail the first security review. This guide is about the decisions around the handler, not the handler itself.

Four decisions to make before any code

Each of these is cheap now and expensive later, because changing them means migrating records that customers can see.

  1. 1What is the primary key? The directory's own identifier, not the email address. Emails change; some are even reassigned.
  2. 2What does removal mean? Deactivation is right for most products, because deleting a user usually deletes their authored content with them.
  3. 3What is the source of truth for names and email? If the directory owns them, your profile editor should be read-only for those fields, or the next sync will silently overwrite what somebody typed.
  4. 4Do groups mean anything? If they map to roles, decide whether the mapping is per organization — it almost always has to be.

The states a directory user moves through

Providers differ in which events they send, but the underlying states are the same. Modelling them explicitly is what stops a suspended person from keeping a live session.

Directory stateEvent you receiveWhat your product should do
Created and assigneddsync.user.createdCreate the account, inactive until first sign-in if you prefer
Attribute changeddsync.user.updatedUpsert the record from the full payload
Suspendeddsync.user.updated with state inactiveBlock sign-in and revoke existing sessions
Unassigned from the appdsync.user.deletedDeactivate; keep the data
Deleted from the directorydsync.user.deletedDeactivate, and follow your retention policy
Added to a groupdsync.group.user_addedRe-evaluate role mapping

Revoking sessions on suspension is the step most often missed. Without it, somebody suspended on Friday afternoon keeps working until their session expires, which may be days.

Backfilling an existing directory

Events describe changes from now on. When a customer connects a directory that already contains two thousand people, you need to read the current state once — and you need it to be safe to run twice, because it will be.

scripts/backfill.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function backfill(directoryId: string) {
6 let after: string | undefined;
7 let seen = 0;
8
9 do {
10 const page = await paycux.directorySync.listUsers({
11 directory: directoryId,
12 limit: 100,
13 after,
14 });
15
16 for (const user of page.data) {
17 // upsert — ayni kaydi iki kez islemek guvenli olmali
18 await upsertUser({
19 directoryUserId: user.id,
20 organizationId: user.organizationId,
21 email: user.emails.find((e) => e.primary)?.value,
22 firstName: user.firstName,
23 lastName: user.lastName,
24 active: user.state === "active",
25 });
26 seen++;
27 }
28
29 after = page.listMetadata.after;
30 } while (after);
31
32 return seen;
33}

Mapping groups to roles

Never infer meaning from a group name. A group called Admins in one customer's directory is their IT department; in another it is every team lead. Guessing produces privilege escalation that nobody notices until an audit.

Build a small screen where an organization admin pairs each incoming group with one of your roles, store it against the organization, and re-evaluate on every membership event. Unmapped groups should grant nothing.

Log the mapping decisions as audit events. When somebody asks in six months why a person had a permission, the answer needs to be a record rather than an inference.

What to test

A provisioning integration is tested by breaking it. These five cases catch nearly everything that reaches production.

  1. 1Create a user, then create the same user again. The second attempt must not produce a duplicate.
  2. 2Suspend a user with an open session and confirm the session stops working, not just the next sign-in.
  3. 3Rename a user in the directory and confirm your records follow the identifier, not the email.
  4. 4Return a 500 from your endpoint, then let the retry succeed. Confirm nothing was applied twice — no duplicate welcome email, no doubled group membership.
  5. 5Bulk-import fifty users at once and confirm your handler keeps up rather than timing out and triggering a retry storm.

What SCIM will not tell you

SCIM carries identity and membership. It does not carry job titles reliably, it does not carry manager relationships in a standard way, and it does not say when somebody's last day is — only that they have gone.

If your product needs employment context rather than directory context, the HR system is the source you want. That is a different integration with a different shape, and it is worth knowing before you promise a customer that SCIM will deliver it.