Concepts
SCIM group membership
Groups are how customers express who should get what. They are also the part of SCIM where providers diverge most: the same act of adding somebody to Engineering can reach you as a group patch, a user update, or both in an order nobody promised. Handling that is mostly a matter of deciding what you treat as authoritative.
Why groups matter more than users
A directory integration that only syncs users answers one question: who works here. That is useful, and it is not what customers buy the integration for. What they want is for the group they already maintain — Engineering, Finance, Contractors, Leavers — to control access in your product without anybody administering a second list.
Group membership is therefore the event that actually grants and revokes access in most designs. A user record arriving is a person who might sign in; a group membership arriving is a person who should be able to do something specific. Wiring your roles to the second rather than the first is what makes the integration worth having.
It is also the part customers notice immediately when it is wrong. Nobody files a ticket because a job title synced late. Somebody files a ticket within the hour when a person removed from a group can still approve invoices.
How the same change reaches you
SCIM allows membership to be expressed from either side, and providers make different choices. Some send a PATCH against the group with an add operation; some send the user with a new groups array; a few send both, in an order that is not guaranteed. The events you receive are normalised, but knowing what happened underneath explains the shapes.
| Event | Carries | What to do |
|---|---|---|
| dsync.group.user_added | The group and the user added to it | Grant whatever the group maps to. This is the primary signal. |
| dsync.group.user_removed | The group and the user removed from it | Revoke promptly. This one has a clock on it. |
| dsync.user.updated | The full user, including its current groups array | Reconcile against the array rather than diffing — it is authoritative as of that moment. |
| dsync.group.created | A new group with no members yet | Relevant only if your role mapping is configured per group. |
| dsync.group.deleted | A group that no longer exists | Revoke everything it granted. Nobody will send you a removal event per member. |
The last row is the one that catches people. Deleting a group at the source does not always produce a removal event for each member, so a handler that only listens for user_removed leaves access in place for everybody who was in it.
Mapping groups to roles
Do not check group names in your request path. Group names belong to the customer, who will rename Engineering to R&D during a reorganisation without telling you, and a permission check written against a string they own is a permission check they can break by accident.
Map instead: each directory group is associated with one of your roles, configured per organization, and your code checks permissions. A rename then changes a display name in one mapping row rather than silently revoking access for forty people.
- 1Read the customer's groups once, after their directory activates, and present them in your own admin interface.
- 2Let somebody — you or the customer — associate each relevant group with a role. Groups with no association do nothing, which is the correct default.
- 3Store the association against the group id, not the group name. The id survives a rename; the name does not.
- 4On a membership event, resolve the group id to a role and update the membership. On a user update, reconcile the whole groups array in one pass.
- 5When a person is in two mapped groups, apply a deterministic rule — highest privilege wins, or an explicit precedence order — and write it down. Ambiguity here becomes a support ticket about why one person has different access from their colleague.
A handler that survives reordering
Deliveries are not strictly ordered, so a handler that applies deltas can end up applying a removal after the addition that superseded it. The way out is to make the handler idempotent and, wherever an event gives you the full picture, reconcile rather than diff.
1export async function handle(event) {2 switch (event.event) {3 case "dsync.group.user_added":4 case "dsync.group.user_removed": {5 // Delta uygulamak yerine gruplarin tamamini yeniden oku — sira bagimsiz olur.6 const user = await paycux.directorySync.getUser(event.data.user.id);7 return reconcileRoles(user.idpId, user.groups.map((g) => g.id));8 }910 case "dsync.user.updated": {11 return reconcileRoles(event.data.idp_id, event.data.groups.map((g) => g.id));12 }1314 case "dsync.group.deleted": {15 return revokeRoleForGroup(event.data.id);16 }17 }18}
Nested groups and what does not arrive
Directories support groups containing groups; SCIM largely does not carry that structure to you. What arrives is usually the flattened membership the provider computed, and sometimes only the direct membership, depending on how the customer configured the push. If a customer says somebody should be in Engineering because they are in Platform Team which is inside Engineering, check what the provider is actually sending before assuming a bug on your side.
The safest position is to treat the memberships you receive as the complete truth and to say so plainly in your documentation. Attempting to reconstruct a hierarchy from partial data produces an access model that disagrees with the customer's directory in ways neither side can reproduce.