Workplace & Collaboration, chapter 08 of 67
Provisioning, SCIM & Offboarding
SCIM 2.0, Entra, Okta and Google directory sync, HR-driven joiner-mover-leaver, deactivation versus deletion, and handing content over at offboarding.
Single sign-on decides who may sign in. Provisioning decides who exists. An enterprise with ten thousand employees hires, moves and loses people every day, and it wants each change to reach every product it uses without an administrator clicking through fifty admin consoles: an account and the right groups on the first morning, new groups when someone changes teams, and a dead account the hour they leave. The source of truth is the HR system and the identity provider; the collaboration product is a target that has to accept changes it didn't originate, at volume, and keep its own state consistent with them.
The hard part is not creating users. It is the edges: a directory that sends a delete when the employer meant a suspension, a group that grants access to a channel full of salary data, a person who leaves while owning the shared drive the finance team runs on, a rehire with the same email and a different employee number, and an identity provider that stops syncing for a week and then sends everything at once. This chapter covers the protocol (SCIM 2.0), how the major identity providers drive it, HR-driven joiner-mover-leaver flows, what deactivation and deletion mean in each product, and how content changes hands when someone leaves. The playbook's Provisioning and Deprovisioning has the generic rule; this is the industry detail.
How the Platforms Do It#
As of October 2026, from each vendor's documentation:
| Product | Provisioning in | Plan | Deprovisioning semantics | Content after removal |
|---|---|---|---|---|
| Slack | SCIM 1.1 and 2.0 API | Business+ or Enterprise | Users can only be deactivated, never deleted; a deactivated user's email blocks re-provisioning until changed | Messages and files stay; the member is removed from channels and signed out |
| Atlassian | SCIM from Okta, OneLogin, Entra ID, Duo, Google, PingFederate, JumpCloud, or the user provisioning API | Atlassian Guard Standard | Deleting a user in the identity provider deactivates the Atlassian account | Deactivated accounts keep roles and groups; deletion after a 14-day grace replaces the name with "Former user" |
| Google Workspace | Google Cloud Directory Sync from Active Directory or LDAP | Not covered here | Suspend (temporary), Archived User license (retain data at lower cost) or delete (restorable for 20 days) | User-owned Drive files, calendars and other data are deleted unless transferred; shared drive files stay |
| Microsoft 365 | Entra ID is the directory; Entra provisioning pushes to SaaS apps over SCIM | Group-based assignment needs Entra ID P1 or P2 | Soft delete for 30 days, then hard delete; SCIM targets get active: false, then a DELETE |
The deleted user's OneDrive is kept 30 days by default for the manager or a secondary owner |
| Notion | SCIM and SAML automatic account creation | Enterprise for SCIM | Not covered here | Not covered here |
Three patterns:
- SCIM is universal, and gated. Every enterprise product speaks SCIM 2.0, and every one charges for it: Slack's Business+ and Enterprise plans, Atlassian Guard, Notion Enterprise.
- Nobody deletes on the first signal. Slack never deletes through SCIM, Atlassian turns an identity provider delete into a deactivation, Microsoft soft-deletes for 30 days, Google keeps a deleted account restorable for 20. The directory's delete is treated as "stop access now," and destruction of the person's record is a separate, later decision.
- Content outlives the person, but who gets it varies. Chat messages stay where they were posted. Files are the hard case, because the suites tie them to a user's storage: Google deletes untransferred files, OneDrive hands them to a manager for 30 days. Products that store content in shared containers by default have an easier offboarding.
SCIM 2.0#
SCIM (System for Cross-domain Identity Management) is two IETF documents published in September 2015: RFC 7643 defines the schema, RFC 7644 the protocol. The product implements a SCIM service provider; the identity provider is the client that pushes changes.
The schema (RFC 7643). Every resource has id (the service provider's identifier), externalId (the client's identifier) and meta. A User has userName, name, emails, active and groups; a Group has displayName and members. The Enterprise User extension, urn:ietf:params:scim:schemas:extension:enterprise:2.0:User, adds employeeNumber, manager, department, costCenter, organization and division. The attribute that matters most is the vaguest: active is "a Boolean value indicating the user's administrative status," and "the definitive meaning of this attribute is determined by the service provider." Every product has to write down what active: false does to sessions, tokens, seats and content.
The protocol (RFC 7644). Endpoints are /Users, /Groups, /Me, /ServiceProviderConfig, /ResourceTypes, /Schemas, /Bulk, and /.search for searches by POST. GET reads, POST creates, PUT replaces ("PUT MUST NOT be used to create new resources"), PATCH changes parts with add, remove and replace operations applied in order and atomically, and DELETE removes. Filters use eq, ne, co, sw, ew, pr, gt, ge, lt and le with and, or and not; list responses page with a 1-based startIndex and count. ETags with If-Match give optimistic concurrency.
What the RFCs leave to the implementer, and where integrations break:
- Matching. The client has to find the existing user before it creates one. Entra's provisioning service queries the target for a user matching a configured attribute (for example
userPrincipalNamemapped touserName), creates one if none matches, and caches the target's id for every later operation. A product that lets the matching attribute change (an email rename) without keepingexternalIdstable gets duplicates. - Group membership at scale. A PATCH that adds 20,000 members to one group is a single request in the protocol and a large transaction in the product. Atlassian publishes recommended limits per connected directory: 1 million users, 290,000 users per group, 250,000 groups and 8 million total memberships.
- What a guest is. Slack's SCIM can't fully provision single-channel guests: they are created as full users and then restricted in Slack's admin pages. Guest status is product-specific and outside the core schema.
- Read-only after sync. Once Atlassian is connected to an identity provider, admins are "unable to make changes to a user's account from the organization," and synced groups can only be edited in the identity provider. The product has to show which fields are mastered elsewhere and refuse edits to them, or the next sync silently reverts them.
How the Identity Providers Drive It#
Microsoft Entra ID
Entra's provisioning service connects to the app's SCIM 2.0 endpoint over TLS 1.2 and runs an initial cycle (read every in-scope user and group, match or create in the target, write a watermark) and then incremental cycles that read changes since the watermark through Microsoft Graph delta queries. Scope is usually assignment-based: users and groups assigned to the app (groups need P1 or P2), with the important limitation that the service can't read nested groups and provisions only immediate members of an assigned group. Attribute-based scoping filters are the alternative.
Deprovisioning has four triggers: a user soft-deleted in Entra (or blocked from sign-in), permanently deleted, unassigned from the app, or moved out of scope. For a SCIM app, the first, third and fourth send a PATCH setting active to false; the hard delete, 30 days after a soft delete, sends a DELETE. Microsoft's own recommendation to app developers is to support both soft and hard deletes "to recover when a user is accidentally disabled."
The failure modes are documented too. Errors are retried in later cycles with backoff. If most calls fail (expired credentials, an endpoint down), the job enters quarantine, incremental cycles slow to once a day, and after four weeks in quarantine the job is disabled. Initial cycles for assigned users are estimated at 0.01 to 0.08 minutes per assigned user, group and membership, and creating everyone takes about twice as long as matching everyone. Every operation is in the provisioning logs, and configuration changes are in the audit log.
Okta
Okta frames the same work as lifecycle management: provisioning and deprovisioning to apps, and profile sourcing. A profile source is the authoritative system for a user's identity, only one source applies to a given profile at a time, and an HR system such as Workday can act as one, sending hire, change and termination events that create, update and deactivate Okta users, which Okta then pushes to downstream apps. Okta warns that enabling both profile sourcing and attribute updates on the same app can cause data loss, because Okta may overwrite the source's attributes: the two-masters problem in one warning.
Google Cloud Directory Sync reads Active Directory or another LDAP server and updates the Google account to match; it is one-way ("data on the LDAP server is never updated or altered"), runs inside the customer's network, covers users, groups and shared contacts, doesn't migrate content (mail, calendar events, files), and has a simulation stage to test rules before a real run. Because the directory wins every disagreement, a mistaken exclusion rule is a mass deprovisioning, which is what the simulation is for. In the other direction, Atlassian lists Google Workspace and Google Cloud among the identity providers that can provision into it over SCIM.
Joiner, Mover, Leaver#
The enterprise view of provisioning starts in HR, not in the identity provider. Microsoft's planning guide for HR-driven provisioning (Workday, SAP SuccessFactors) defines the joiners-movers-leavers process as "new hires, transfers, and termination by using a cloud HR app as a system of records": a termination in the HR app disables the user in Active Directory and Entra ID automatically, and the identity provider's outbound provisioning carries the change to every connected SaaS app. Its planning questions are the ones every product's provisioning design should answer:
- Joiners. Future-dated hires are created before their start date, so the target product sees an account that shouldn't be usable yet, and a rescinded offer has to remove an account that was never used. Rehires raise the question of whether the old employee id comes back with the old account.
- Movers. A transfer changes department, manager and groups, so access granted by the old groups has to go when the new groups arrive. The product receives this as group membership PATCHes; nothing in SCIM tells it that two changes are one transfer, so a grant removed before its replacement is added produces a short outage, and the reverse produces a short overgrant.
- Leavers. Effective dates, contingent workers handled differently from employees, and what "terminated" means at midnight in which time zone. The product's job is to make
active: falsetake effect everywhere at once: sessions, API tokens, OAuth grants, scheduled automations running as the person, and seat counts.
My default for the target side: treat every SCIM deactivation as an incident-grade revocation (sessions and tokens killed within minutes), run the content handover as a separate, slower workflow that an admin can review, and never require the identity provider to send a hard delete for anything other than erasing the person's personal data.
Deactivation Versus Deletion#
Each product means something specific by each word:
- Slack. Deactivated people aren't notified, "nor are their messages or files deleted"; they are removed from all channels, signed out on every device and blocked from signing back in, and can be reactivated. SCIM can only deactivate. Billing follows: under Slack's Fair Billing Policy, when an admin deactivates a member Slack deposits a prorated credit the next day for the unused part of the billing period, and a member inactive for more than 28 days is credited too; reactivating charges the prorated cost again.
- Atlassian. Deactivating a managed account retains its roles and group memberships so it can come back. Deleting "permanently close[s] an Atlassian account": the account is deactivated for a 14-day grace period that can't be shortened (only organization admins can cancel), personal data is removed from account services within 30 days, content the person created (pages, issues, comments) stays, and the name becomes "Former user." Billing for a deleted account stops, and resumes if the deletion is cancelled.
- Google Workspace. Suspension blocks access temporarily. An Archived User license keeps a former employee's data at lower cost. Deletion removes access to all services and data, and "data owned solely by the user, that isn't transferred before deleting their account, is permanently deleted": Gmail, owned Drive files, Sites, the primary calendar and owned shared calendars among them. A deleted account can be restored for up to 20 days. On the Annual/Fixed-Term plan deleting a user doesn't reduce the license count.
- Microsoft 365. A deleted user stays in the admin center for 30 days. Removing a license or blocking sign-in does not start OneDrive cleanup; only deleting the account from Entra ID does.
Two consequences for anyone building a product:
- Deactivation must be reversible and complete. Reversible because directories make mistakes (a scoping filter edited, a group renamed); complete because a deactivated account with a live API token or a running automation is still acting. Microsoft's guidance, Slack's reactivation and Atlassian's retained roles all say the same thing.
- Deletion is about the person's personal data, not the work. Atlassian's split is the model: the account and its personal data go; pages, issues and comments stay, attributed to "Former user." Erasing a departed employee's authored work would destroy the employer's records and break every thread they took part in. Where personal data is embedded in content the employee typed, Atlassian keeps it; the employer, as controller, decides about it under its own retention rules (Erasure vs Append-Only Records).
Content Ownership on the Way Out#
The hardest offboarding question is who owns what the person owned. The products split by where they put content by default.
- Personal storage by default (Google Drive, OneDrive). In Google Workspace, super admins can transfer a user's Drive and Docs files, primary calendar and Data Studio assets during deletion; other admins must transfer before deleting; group calendars must be transferred first because deleting the owner deletes them; private files may move anyway if they sit in a shared folder; and files in shared drives aren't affected because "your organization owns these files, not users." In Microsoft 365, the deleted user's manager is given access to their OneDrive by default (or a secondary owner, if no manager is set), told it will be deleted at the end of the retention period, reminded seven days before, and the OneDrive then sits in the recycle bin for 93 days. The default retention is 30 days and is configurable. Retention policies and holds override this schedule: content can be kept longer, and a OneDrive under an eDiscovery hold isn't deleted until the hold is removed. Unlicensed OneDrive accounts are archived on their 93rd unlicensed day.
- Shared containers by default (chat, wikis, trackers). A Slack message lives in a channel; a Confluence page lives in a space; a Jira issue lives in a project. Nothing has to move, but responsibility does: the open issues assigned to the person, the pages they own, the saved filters and dashboards the team relies on, the automation rules that run as them. Atlassian's guidance for Jira is a search for
assignee in (inactiveUsers())followed by a bulk change of the assignee field; filters, dashboards and project leads need their own reassignment.
My default for a builder: put content in containers the organization owns, keep an owner (a responsible person) separate from the author (who wrote it), and generate an offboarding worklist on deactivation, with every container the person owns, every item assigned to them, every automation and integration acting as them, and every share link they created, so an admin (or the departing person's manager) reassigns before the deletion clock runs out.
How to Build It#
The core data model's members (kind, role, status, deactivated_at) and groups with group_members (source local, scim or directory) are where provisioning lands. This chapter adds the directory connection, the external ids and mastering, a log of SCIM operations, and offboarding.
CREATE TABLE directory_connections ( -- one per identity provider pushing to the organization
id UUID PRIMARY KEY DEFAULT uuidv7(),
organization_id UUID NOT NULL REFERENCES organizations (id),
provider TEXT NOT NULL, -- 'entra', 'okta', 'google', 'onelogin', 'custom'
scim_version TEXT NOT NULL DEFAULT '2.0' CHECK (scim_version IN ('1.1', '2.0')),
token_hash BYTEA NOT NULL, -- bearer token for the SCIM endpoint, stored hashed
token_rotated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
mastered_fields TEXT[] NOT NULL DEFAULT '{name,emails,title,department,manager,active}',
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'paused', 'disconnected')),
last_request_at TIMESTAMPTZ
);
ALTER TABLE members
ADD COLUMN directory_connection_id UUID REFERENCES directory_connections (id),
ADD COLUMN external_id TEXT, -- SCIM externalId from the identity provider
ADD COLUMN employee_number TEXT, -- Enterprise User extension
ADD COLUMN manager_member_id UUID REFERENCES members (id),
ADD COLUMN scim_etag TEXT, -- version for If-Match
ADD COLUMN deactivation_source TEXT CHECK (deactivation_source IN ('admin', 'scim', 'hr',
'inactivity', 'self')),
ADD CONSTRAINT members_external_id_unique UNIQUE (directory_connection_id, external_id);
ALTER TABLE groups
ADD COLUMN directory_connection_id UUID REFERENCES directory_connections (id),
ADD COLUMN external_id TEXT,
ADD CONSTRAINT groups_external_id_unique UNIQUE (directory_connection_id, external_id);
CREATE TABLE scim_operations ( -- every request the identity provider sent, as received
id UUID PRIMARY KEY DEFAULT uuidv7(),
directory_connection_id UUID NOT NULL REFERENCES directory_connections (id),
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
method TEXT NOT NULL, -- GET, POST, PUT, PATCH, DELETE
resource_type TEXT NOT NULL CHECK (resource_type IN ('User', 'Group', 'Bulk')),
resource_id UUID, -- our member or group id, once resolved
request_body JSONB, -- personal data: purged on the retention schedule
status_code SMALLINT NOT NULL,
error_detail TEXT,
purge_after TIMESTAMPTZ NOT NULL
);
CREATE TABLE offboarding_runs ( -- the worklist generated when a member is deactivated
id UUID PRIMARY KEY DEFAULT uuidv7(),
member_id UUID NOT NULL REFERENCES members (id),
triggered_by TEXT NOT NULL CHECK (triggered_by IN ('scim', 'admin', 'hr')),
access_revoked_at TIMESTAMPTZ, -- sessions, tokens, OAuth grants, automations stopped
delete_after TIMESTAMPTZ, -- end of the grace period, if deletion was requested
state TEXT NOT NULL DEFAULT 'revoking' CHECK (state IN ('revoking', 'awaiting_handover',
'handed_over', 'deleted', 'cancelled')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE ownership_transfers ( -- one row per thing that changes hands
id UUID PRIMARY KEY DEFAULT uuidv7(),
offboarding_run_id UUID NOT NULL REFERENCES offboarding_runs (id),
object_type TEXT NOT NULL CHECK (object_type IN ('container', 'item', 'work_item_assignment',
'saved_query', 'automation', 'app_installation', 'share_link', 'api_token')),
object_id UUID NOT NULL,
to_member_id UUID REFERENCES members (id), -- null when the action is revoke, not transfer
action TEXT NOT NULL CHECK (action IN ('transfer', 'revoke', 'keep_as_former')),
done_at TIMESTAMPTZ,
done_by UUID REFERENCES accounts (id)
);
Notes on the sketch:
externalIdis the match key, never the email. Emails change on marriage, rebrand and rehire; the identity provider'sexternalIddoesn't. The unique constraint per connection is what prevents duplicates when a client retries a POST.- Mastered fields are enforced in the product. The admin UI and API refuse edits to fields in
mastered_fieldsfor members bound to a connection, and show where the field comes from, so the next sync can't silently revert a manual change. active: falserevokes synchronously; deletion runs on a clock. The SCIM handler setsmembers.status, kills sessions and tokens, pauses automations and writesoffboarding_runs.access_revoked_atin the same transaction as the response;delete_afteris set only by a DELETE or an admin, after a grace period, and the deletion removes personal data while items stay attributed to a former member.- Group PATCHes are applied in batches with idempotent membership writes. A 20,000-member PATCH is split into chunks inside one logical operation, and
group_memberswrites are upserts, so a retry after a timeout converges instead of failing. - The SCIM log is personal data with its own retention. It is invaluable for "why was this person removed?" and should be purged on a schedule like any log of personal data.
- Events:
member.provisioned,member.updated,member.deactivated,member.reactivated,group.membership_changed,offboarding.access_revokedandoffboarding.handed_overgo to the audit log, billing (seats), search (re-trim results for the removed person) and notifications (the manager's handover worklist).
Auditing#
- Every SCIM request and its outcome, with the connection, the resolved member or group and the error, kept long enough to answer disputes (Typed Actors records the identity provider as the actor, not a person).
- Mastering changes: connections created, tokens rotated, mastered fields changed.
- Deactivations and reactivations with their source, and the time access was actually revoked.
- Ownership transfers: every object handed over, to whom, by whom, and what was revoked instead.
- Deletions: the grace period, who requested and who cancelled, and what personal data was removed.
What This Means for the System You Build#
- Implement SCIM 2.0 completely: stable
externalIdmatching, PATCH with ordered atomic operations, ETags, paging, and a/ServiceProviderConfigthat tells the truth. - Write down what
active: falsemeans and make it revoke sessions, tokens, OAuth grants and automations at once. - Treat the directory's delete as deactivation; delete personal data after a grace period and keep authored content attributed to a former member.
- Enforce mastered fields and show admins where each value comes from.
- Handle the mover case: group changes arrive as separate PATCHes, so make grants idempotent and avoid long windows of over- or under-access.
- Store content in organization-owned containers and generate an offboarding worklist of everything the person owned or ran.
- Expect quarantine and floods: idempotent writes, batching and clear errors, because identity providers retry and then catch up all at once.
References#
Standards
- RFC 7643: SCIM Core Schema, common attributes, User and Group attributes, the Enterprise User extension, the definition of
active - RFC 7644: SCIM Protocol, endpoints, methods, PATCH operations, filters, ETags, paging, September 2015 publication
Vendor documentation
- Slack: SCIM API, Business+ and Enterprise, SCIM 1.1 and 2.0, deactivation only, email blocking re-provisioning, single-channel guests
- Slack: Deactivate a member's account, messages and files kept, channels removed, sign-out, reactivation, who can deactivate
- Slack: Fair Billing Policy, the 28-day inactivity rule, prorated credits on deactivation and charges on reactivation
- Atlassian: Understand user provisioning, Guard Standard, supported identity providers, read-only synced users and groups, deactivation on IdP delete, recommended sync limits
- Atlassian: Delete a managed account, deactivate versus delete, 14-day grace, 30-day personal data removal, content kept, "Former user", billing
- Atlassian: How to remove inactive users from the backlog,
assignee in (inactiveUsers())and bulk reassignment - Microsoft Entra: How application provisioning works, SCIM over TLS 1.2, matching, initial and incremental cycles, scoping and nested groups, deprovisioning triggers, 30-day hard delete, quarantine
- Microsoft Entra: When will provisioning finish for a specific user, cycle time estimates, provisioning and audit logs
- Microsoft Entra: Plan cloud HR application to Microsoft Entra user provisioning, joiners-movers-leavers, automatic disable on termination, rehires, rescinds and effective dates
- Okta: About profile sourcing, the authoritative source per profile, Workday as a source, the warning about pushes to a source app
- Google Workspace: About Google Cloud Directory Sync, one-way sync from AD or LDAP, objects covered, no content migration, the simulation stage
- Notion: Setting up and managing an organization, SCIM provisioning and SAML in Enterprise organization settings, suppressing SCIM invite emails in phased rollouts
- Google Workspace: Delete or remove a user from your organization, suspend, archive and delete, what is deleted, 20-day restore, transfers during deletion, shared drives, licenses
- Microsoft: OneDrive retention and deletion, manager and secondary owner access, 30-day default, seven-day reminder, 93-day recycle bin, holds and retention precedence, archiving on day 93 unlicensed