VibeWeek
Home/Grow/SCIM Provisioning & Directory Sync UI: Chat Prompts

SCIM Provisioning & Directory Sync UI: Chat Prompts

⬅️ Back to 6. Grow

When you sell to enterprise customers, "users sign up themselves" stops working. Their IT requires central control: when an employee joins, they get auto-provisioned in your product (with right role); when they leave the company, they're auto-deprovisioned (immediately, no manual deactivation). The standard for this is SCIM (System for Cross-domain Identity Management) — an API spec their IdP (Okta / Azure AD / Google Workspace / OneLogin / JumpCloud) calls into your product to create / update / delete users + groups.

SCIM is the foundational requirement for enterprise SaaS. Without it, enterprise IT teams won't deploy you broadly (they'd have to manage your user list manually). With it, you become enterprise-friendly.

This is distinct from SSO / Enterprise Auth (which is authentication — proving who someone is). SCIM is provisioning — managing the lifecycle of who exists in your system. Both matter; many enterprise customers demand both.

When This Belongs

Use SCIM provisioning when:

  • You sell to mid-market+ / enterprise (Okta / Azure AD shops)
  • Customers asking "do you support SCIM?" in security questionnaires
  • Manual user management at customer side becomes pain point
  • You support workspaces with 100+ users

Don't bother when:

  • SMB / self-serve product
  • Single-team usage
  • Pre-revenue

What SCIM Does

The IdP (Okta / Azure AD / etc.) becomes the source of truth for users. When IT changes things in the IdP, it pushes to your product:

  • User created: when someone joins customer's company, IdP pushes user create to your product
  • User updated: name change, email change, department change, role attribute change
  • User deactivated: when someone leaves the company, IdP pushes deactivation
  • Group created / updated / deleted: groups in IdP map to roles or workspaces in your product
  • Group membership changed: user added to / removed from group

Your product implements an HTTP endpoint that follows the SCIM 2.0 spec; IdP calls it.

Build vs Buy

For most B2B SaaS, the build/buy decision is decisive.

Buy: WorkOS Directory Sync (Default Recommendation)

WorkOS abstracts SCIM across all major IdPs. You implement a single webhook endpoint; WorkOS handles the SCIM protocol for Okta / Azure AD / Google Workspace / OneLogin / JumpCloud / etc. You receive normalized webhooks: "user created in Okta" → you create user in your DB.

Strengths:

  • One integration covers 20+ IdPs
  • Handles edge cases (different IdPs implement SCIM slightly differently)
  • ~2-week implementation
  • $$ pricing scales with customer count

Pricing: ~$125-500/month per connected customer organization (varies).

Buy: Auth0 Enterprise / Clerk Enterprise / Stytch / Frontegg

If you're already using these for SSO, they often include SCIM as part of enterprise tier.

Build: Direct SCIM Implementation

You implement SCIM 2.0 endpoints directly. Most code:

  • POST /Users (create user)
  • PUT /Users/:id (full update)
  • PATCH /Users/:id (partial update)
  • DELETE /Users/:id (deactivate)
  • GET /Users (list with filters)
  • POST /Groups, etc.

Strengths:

  • No per-customer fee
  • Maximum control
  • Customizable to your data model

Weaknesses:

  • 4-8 weeks of engineering work
  • Each IdP has quirks (Okta vs Azure AD)
  • Ongoing maintenance as IdPs update

For most B2B SaaS at $5M-50M ARR, WorkOS is the right call. Cost is small relative to enterprise deal sizes; engineering time saved is significant. Build direct only at enterprise scale ($100M+ ARR) where the per-customer fees become meaningful.

Architecture (with WorkOS)

I'm building SCIM provisioning for my B2B SaaS using WorkOS Directory Sync.

Architecture:
1. Customer admin sets up Directory Sync in WorkOS (their Okta connects to our WorkOS account)
2. WorkOS receives SCIM events from their Okta
3. WorkOS sends normalized webhooks to our endpoint
4. Our endpoint processes: create / update / deactivate users + groups in our DB

Implement:
1. WorkOS account + Directory Sync configured
2. Webhook endpoint at POST /api/webhooks/workos with HMAC verification
3. Event handlers:
   - dsync.user.created → create user in our DB
   - dsync.user.updated → update user fields
   - dsync.user.deleted → deactivate user (don't delete; preserve audit history)
   - dsync.group.created / updated / deleted → manage roles or workspaces
   - dsync.group.user_added / removed → update group membership
4. Idempotency: same event delivered twice doesn't duplicate
5. Retry logic: WorkOS retries on webhook failure; we handle gracefully

Stack: Next.js + WorkOS SDK + Drizzle.

User Mapping

The trickiest part: mapping IdP attributes to your data model.

Map SCIM attributes to my user model:

Standard SCIM user attributes:
- userName (typically email)
- name.givenName, name.familyName, name.formatted
- emails (primary)
- active (boolean — true = enabled; false = deactivated)
- title
- department
- employeeNumber
- locale
- timezone

Custom enterprise attributes (per IdP):
- managerId
- costCenter
- division
- custom org-specific fields

Mapping decisions:
- Email is unique identifier (email = primary key in our user table?)
- Or use SCIM-provided externalId (more stable; emails can change)
- Department/Title → metadata or role mapping?
- Locale → user preference

Recommended:
- Store externalId (SCIM-provided UUID) as stable identity
- Store email separately; allow email to change
- Capture department / title as metadata
- Map specific groups to specific roles

Show me the user model + mapping logic.

Stack: Drizzle.

Group / Role Mapping

Map IdP groups to roles or workspaces in my product:

Common patterns:

Pattern A: Group-to-Role
- IdP has groups: "saas-admins", "saas-members", "saas-viewers"
- Map: group "saas-admins" → user role 'admin' in my workspace
- When user added to group → grant role
- When user removed → revoke role

Pattern B: Group-to-Workspace
- IdP has groups: "engineering-team", "marketing-team"
- Map: group "engineering-team" → membership in "Engineering" workspace
- Customer admin maps groups to workspaces in our admin UI

Pattern C: Hybrid
- Some groups → roles
- Some groups → workspaces
- Rest ignored

Implementation:
- Mapping table: scim_group_id → my_role | my_workspace
- Customer admin configures mapping in our UI
- On group membership change: re-evaluate user's roles + workspace memberships

UI for customer admin:
- "/admin/scim-mapping"
- List of synced groups from their IdP
- Drop-down to map each to: role / workspace / both / none
- Save + apply

Stack: Next.js + Drizzle + WorkOS API.

Customer Setup Experience

The UX for customer admins to enable SCIM.

Build the customer-facing SCIM setup flow:

1. Workspace admin goes to Settings > Security > Directory Sync
2. Click "Enable SCIM provisioning"
3. We create WorkOS Directory Sync connection; receive setup URL
4. Show step-by-step instructions:
   - "1. Log into your IdP (Okta / Azure AD / etc.)"
   - "2. Add our SAML/SCIM application"
   - "3. Copy this URL: <SCIM endpoint>"
   - "4. Copy this token: <Bearer token>"
   - "5. Configure attribute mapping (we provide template)"
6. Customer admin does the work in their IdP
7. We auto-detect first sync; show confirmation: "Connected to your Okta. Synced X users."
8. Customer admin maps groups to roles / workspaces
9. Activate — new users from IdP auto-provisioned

Per-IdP guides:
- Okta-specific instructions (with screenshots)
- Azure AD specific
- Google Workspace specific
- OneLogin
- JumpCloud
- Generic SCIM (for unsupported IdPs)

Tools / docs:
- Pre-built application templates in Okta marketplace
- Screen-recorded video walkthroughs

Stack: Next.js + WorkOS + your docs.

Deactivation Behavior

The most consequential SCIM event: deactivation.

Decide what happens when SCIM marks user as inactive:

Options:

Option A: Hard deactivate immediately
- User can't log in
- Sessions revoked
- Data preserved (per retention policy)
- Most enterprise-aligned (security teams want immediate offboarding)

Option B: Soft deactivate with grace
- User flagged inactive
- Sessions revoked but data accessible by admins
- Hard delete after X days

Option C: Read-only mode
- User can log in but can't make changes
- Useful if customer wants former employee to wrap up before full deactivation

Recommended: Option A (immediate) is enterprise-standard. Option B/C if customer requests.

Implementation:
1. Receive dsync.user.deleted webhook
2. Set user.deactivated_at = NOW()
3. Revoke all sessions (delete from sessions table; invalidate JWTs)
4. Block API key usage (mark API keys associated with user as revoked)
5. Audit log entry: "User deactivated via SCIM by [IdP system]"
6. Notification to workspace admins (optional; batch daily summary)

Edge case: deactivated user is sole admin
- Block deactivation if user is sole admin (return error to SCIM)
- Force customer to designate new admin first
- Or: auto-promote earliest-tenured admin

Stack: Next.js + your auth + Drizzle.

Group Membership Changes

Handle dynamic role changes via group membership:

Scenario: User in IdP gets moved from "saas-members" to "saas-admins":
1. WorkOS receives group membership change events
2. Webhooks to our endpoint:
   - dsync.group.user_removed (saas-members)
   - dsync.group.user_added (saas-admins)
3. We update user role in real time

Edge cases:
- User in multiple groups mapped to different roles → take highest
- User removed from group not mapped → no-op
- User in admin group + removed → demote to default member role
- Race condition: user removed + added simultaneously → settle to final state

Audit:
- Log every role change via SCIM in audit log
- Surface to customer admin: "Role changed for X via Okta sync"

Stack: Next.js + Drizzle + audit log.

Common Pitfalls

Building SCIM directly when WorkOS would suffice. 4-8 weeks of dev for marginal benefit. Use WorkOS.

Forgetting idempotency. Same webhook delivered twice; user created twice. Use externalId as stable key.

No retry handling. Webhook fails; WorkOS retries; race condition causes inconsistent state. Plan retries.

Mapping email as primary key. Email changes (marriage, role change); breaks linkage. Use externalId.

Missing deactivation handling. Former employees still have active sessions. Revoke on deactivation.

Blocking IdP traffic with rate limits. WorkOS retries on 429s but enterprise customer's first sync might push 1000 users. Allow burst capacity.

No customer-facing setup guide. IT teams give up halfway through SCIM config. Provide per-IdP step-by-step.

Ignoring custom attributes. Customer wants department-based access; you ignored department field. Map at minimum: department + title.

Group mapping not configurable by customer. You hardcode IdP group names; doesn't fit other customer's groups. Make mapping per-customer.

Sole-admin deactivation. SCIM deactivates only admin; workspace orphaned. Block + alert.

No audit log of SCIM events. Investigation: "who got admin role last week?" — answer unclear. Log all SCIM-driven changes.

Sync delays not surfaced. New employee added in IdP; can't access product for hours. Show last-sync time + force-sync option.

No way to test before activation. Customer activates SCIM; users disappear because of mapping bug. Provide test mode.

Webhook signature verification skipped. Anyone can POST as WorkOS. Always verify HMAC.

Stale connections after IdP changes. Customer changes IdP config; sync breaks; nobody notices. Health-check + alert.

Edge cases per IdP not handled. Okta sends slightly different payload than Azure AD; one breaks. Test all major IdPs.

No bulk import for first sync. Customer activates SCIM with 5K existing users; sync takes hours; partial state. Bulk import path.

Privacy concerns ignored. Customer's IdP sends SSN as employeeNumber attribute; you store + leak. Filter sensitive attributes.

Error responses not SCIM-compliant. SCIM spec defines specific error formats; you return generic 500s; IdP confused. Follow SCIM error spec.

No way to disable SCIM mid-flight. Customer wants to pause sync while restructuring; you don't support pause. Add admin toggle.

Forgetting groups → workspace mapping. Only support groups → roles; multi-workspace customers struggle. Support both.

See Also