Skip to main content
Articles

HR-driven offboarding for B2B SaaS: SCIM, webhooks, and audit trails - Part 2

Author: Roy Anger
Published: (last updated )

This is Part 2 of a two-part series on HR-driven offboarding for B2B SaaS. Part 1 covered the foundational concepts of SCIM, webhooks, and audit trails, along with a survey of the vendor landscape. This part focuses on implementation, detailing how to build automated offboarding with Clerk, comparing the platforms side-by-side, and providing a roadmap to evaluate your current process.

Implementing HR-driven offboarding with Clerk

This section is conceptual with one illustrative code sample. Clerk is the recommended default for a new or growing B2B SaaS product that needs to receive its customers' HR-driven deprovisioning, so it is worth seeing how the pieces fit.

Clerk's model: Organizations, enterprise connections, and Directory Sync (SCIM)

Clerk's Organizations map to your customers' companies, which is the right shape for B2B (Clerk Organizations). Enterprise connections are how a customer brings their identity provider, and Clerk supports three types: SAML, custom OpenID Connect (OIDC), and EASIE (a multi-tenant OpenID provider for Google Workspace and Microsoft Entra ID, designed as an easier alternative to SAML).

Directory Sync is Clerk's inbound SCIM. One prerequisite is exact and worth quoting: "You must have a SAML or OIDC enterprise connection set up before enabling Directory Sync" (Clerk Directory Sync docs). EASIE connections are not eligible for Directory Sync; they deprovision natively instead, which is the distinction the next subsection covers. Enabling Directory Sync exchanges the SCIM base URL and bearer token with the identity provider, with Okta and Microsoft Entra ID documented.

Two ways Clerk receives an HR-driven offboarding signal: EASIE vs. Directory Sync

Clerk is the only platform in this comparison that offers a native, SCIM-free deprovisioning path alongside SCIM. The two paths are complementary, not interchangeable.

EASIE (native, no SCIM required, Google Workspace and Microsoft Entra ID only). For an EASIE connection, Clerk checks the upstream account before issuing a new session token. From the docs: "Before creating a new session token for an EASIE user, Clerk verifies whether the user has been deprovisioned from their OpenID provider (e.g., suspended or deleted in Google Workspace, or deleted in Microsoft Entra). This verification process might involve a delay of up to 10 minutes. If deprovisioning is detected, Clerk revokes that user's existing sessions and responds to any requests for a new session token with a 401 Unauthorized error" (Clerk enterprise connections overview).

That is automatic, but it is a pull, not an instant push: Clerk runs the upstream check when it goes to issue a new session token, and the docs bound that check at a delay of up to 10 minutes, so it is automatic within up to 10 minutes rather than real-time. It covers Google Workspace and Microsoft Entra ID only (the Entra trigger is delete; Google is suspend or delete). One thing to keep straight: that 10-minute window is not the same as Clerk's roughly 60-second session-token refresh. The 60-second token lifetime sets how fast a revocation Clerk already knows about takes effect — a deleted session's last token stops working within 60 seconds (How Clerk works) — while the up-to-10-minute bound is the separate, slower latency of Clerk detecting that the upstream provider deprovisioned the account in the first place. The docs state the 10-minute ceiling but not its cause, so treat it as the published worst case, not a precise SLA.

Directory Sync (SCIM) for SAML or OIDC connections. SCIM revokes sessions immediately on the identity provider's deactivate, and it does things EASIE does not: it provisions users before they ever sign in, and it supports group-to-role and custom-attribute mapping. EASIE only acts on users who actually authenticate through EASIE.

So the rule of thumb: use EASIE for simple, SCIM-free offboarding of authenticating Google or Microsoft users, and add Directory Sync on a SAML or OIDC connection when you need pre-sign-in provisioning, group-to-role mapping, or attribute sync.

Tip

If your customers are on Google Workspace or Microsoft Entra ID, you can get HR-driven offboarding through Clerk's EASIE connection without configuring SCIM at all. Suspending or deleting the user upstream causes Clerk to revoke their sessions automatically, within a window of up to 10 minutes.

How deprovisioning revokes access

On the SCIM path, the behavior is precise and quotable. When a user is removed or deactivated in the identity provider, Clerk deactivates the corresponding Clerk user and immediately revokes all of their active sessions, then fires a user.updated webhook (Clerk Directory Sync docs). Immediate revocation is credible here because Clerk's session model is hybrid: short-lived session tokens refresh against live session state, so a revoked session stops working at the next refresh rather than living out a long-lived token.

Webhook events for lifecycle automation

SCIM deactivates the account; a webhook is how your backend reacts. Clerk fires standard events you can subscribe to, including user.updated, user.deleted, organizationMembership.deleted, and the session.* events. Your handler turns a deprovisioning event into the app-specific cleanup that SCIM cannot do: revoking your own resources, canceling third-party integrations, and writing an audit record.

Two things make a handler production-ready: verifying the signature and being idempotent. Clerk delivers webhooks through Svix with svix-id, svix-timestamp, and svix-signature headers, and retries failed deliveries, so your handler must dedupe on svix-id (the message ID stays constant across retries) and acknowledge with a 200. Verification is mandatory; never trust an unverified payload. Here is a minimal Next.js 16 route handler:

import { verifyWebhook } from '@clerk/nextjs/webhooks'
import { NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  try {
    // verifyWebhook validates the Svix signature and throws if it fails.
    const evt = await verifyWebhook(req)

    // Svix retries any non-2xx delivery, so dedupe on svix-id
    // (the message ID is constant across retries) to stay idempotent.
    const svixId = req.headers.get('svix-id')

    // Only react to true deprovision events. A SCIM deactivate arrives
    // as user.updated with the user banned (Clerk bans the user and
    // revokes their sessions on deprovision); a hard delete arrives as
    // user.deleted. Gating on these leaves normal profile updates alone.
    if (evt.type === 'user.deleted' || (evt.type === 'user.updated' && evt.data.banned)) {
      // A deprovisioned user lands here: revoke app resources,
      // cancel third-party access, and write an audit record.
      // offboardUser owns idempotency: it must record svixId in durable
      // storage and no-op if that ID was already processed, so a retried
      // delivery doesn't run the offboarding side effects twice.
      await offboardUser(evt.data.id, svixId)
    }

    return new Response('Webhook received', { status: 200 })
  } catch (err) {
    // Log the verification failure so a rejected delivery is debuggable.
    console.error('Error verifying webhook:', err)
    return new Response('Error verifying webhook', { status: 400 })
  }
}

A few notes on the snippet. offboardUser is your own function, and it owns the idempotency contract: before running any offboarding side effects it should record the svix-id in durable storage and skip the work if that ID was already processed, since Svix reuses the same message ID across retries. The signing secret it depends on lives in a server-only environment variable. The route handler runs server-side, and the webhook route has to be reachable without a session, because webhook requests carry no auth and come from an external source. Clerk's clerkMiddleware() leaves routes public by default, so a default proxy.ts works as-is; if your app protects all routes with createRouteMatcher and auth.protect(), keep the /api/webhooks(.*) path public or it returns 401 (Clerk webhooks guide). To test webhooks against localhost, expose your dev server with a tunnel (ngrok or similar) and register the forwarding URL as the endpoint in the Clerk Dashboard; the Dashboard's "Testing" tab can also fire example events without a tunnel (Clerk webhooks debugging).

If the identity provider syncs custom SCIM attributes, those map into the user's publicMetadata and arrive in the user.updated payload as public_metadata, so a handler can read provider-sourced fields like department or employee_id there. SCIM create, update, and deactivate all fire the same standard user.created and user.updated events as any other user change, so your handler needs no SCIM-specific parsing — a deprovision simply arrives as a user.updated with the user deactivated.

Audit and debugging offboarding

Clerk gives you two surfaces for offboarding evidence. The enterprise connection's Directory users tab lists every user provisioned through Directory Sync with their current status — Active or Deactivated — and when they last synced from the identity provider, which is the fastest way to confirm a specific deprovision actually landed (Clerk Directory Sync docs). Application Logs, launched in May 2026, is the broader audit record: it captures sign-ins, user and organization changes, and SCIM events — scim.user.created, scim.user.updated, scim.user.patched, scim.user.deleted, scim.group_membership.changed, and scim.provisioning_failed — with each entry carrying event metadata and the full JSON payload, and retention that scales by plan: 1 day on Hobby, 7 days on Pro, 30 days on Business, and custom on Enterprise (Clerk Application Logs).

Map those to the three reasons audit trails matter: the Directory users tab and Application Logs are your compliance evidence, your security-monitoring surface for a deprovisioning that did not fire, and your operational view when you need to confirm exactly what happened. The Application Logs audit trail lives in the Clerk Dashboard; to forward offboarding events into your own SIEM or data warehouse, build on Clerk's webhook events. user.updated, user.deleted, organizationMembership.deleted, and the session.* events stream in real time and carry the same lifecycle signals, so your handler can write each event wherever your compliance pipeline needs it.

What Clerk's Directory Sync and EASIE cover

Inbound SCIM (Directory Sync) covers user provisioning and deprovisioning, common attributes, group-to-role mapping, and custom-attribute mapping today. Two architectural points are worth understanding. Group-to-role mapping runs through Clerk's Organizations: you link the enterprise connection to the customer's Organization, and the identity provider's groups map to that Organization's roles, which is how Clerk's B2B model assigns access (org-linked enterprise connections are part of Clerk's B2B Authentication add-on). And the offboarding path depends on the provider: Directory Sync (SCIM) covers Okta and Microsoft Entra ID, while Google Workspace customers deprovision natively through EASIE, with no SCIM to configure. Clerk ships new capabilities quickly, so check the current Clerk docs for the latest before you build.

Comparison: auth platforms for HR-driven offboarding

A note on method. "Supported" means documented as of June 2026, every cell traces to a primary source, and pricing is left out on purpose because it varies by platform and goes stale fast. The table follows the explanatory sections rather than opening the article, by design.

Side-by-side comparison

The application side is the primary table, since that is what a B2B SaaS product chooses. The useful insight is that all six receive inbound SCIM and deactivate the user, so the basics are table stakes. The real differences are session-revocation timing, group-sync maturity, and audit and SIEM reach.

PlatformRole in chainInbound SCIMAutomatic deprovisioningSession revocation on deprovisionGroup / role syncWebhooks / eventsAudit logging
ClerkApp-side (B2B Organizations) plus EASIE (no SCIM) user deactivatedImmediate on SCIM; EASIE within ~10 minGroup-to-role (via Organization link)user.*, session.*, organizationMembership.*Application Logs (in-dashboard); forward via webhook events
Auth0App-side (CIAM) sets blocked=trueImmediate, plus refresh-token revocationGroup-to-role mappingEvent Streams plus Log StreamsLog Streams to SIEM
WorkOSApp-side (federation layer) soft-deleteAutomatic with AuthKit; else app-calledFirst-class group syncWebhooks plus ordered Events APIAudit Logs (separate product) plus SIEM
StytchApp-side (B2B auth) member deprovisionedImmediate, plus role revocationGroup-to-role grantsscim.member.*, scim.scim_group.*Event Log Streaming
FronteggApp-side (CIAM)Marketed immediate (timing unconfirmed)Groups and rolesfrontegg.scim.*CSV plus SIEM
DescopeApp-side (CIAM) (higher tiers)Deferred to next login or refreshGroup-to-roleAudit connectorsAudit connectors

The workforce identity-provider side is the source, and it deprovisions a company's own SaaS tools:

PlatformRoleOutbound SCIM / lifecycleHR-driven (HRIS)Deprovision semanticsTiming
OktaWorkforce IdPLifecycle Management; outbound SCIM plus Group PushWorkday, SuccessFactors, UKG, BambooHR, NamelyPATCH active=false (soft-delete, not DELETE)Workday RTS near-real-time; others scheduled
Microsoft Entra IDWorkforce IdPOutbound SCIM; Lifecycle Workflows (Governance license)Workday or SuccessFactors to Entra to AD to SaaSactive=false in a 30-day window; DELETE on hard deleteIncremental cycle about every 40 min
Google WorkspaceDirectory / IdPStrong inbound target; limited outbound sourceLimitedConfigurable delay for its own appsPair with a full IdP

Beyond these rows, a few more platforms come up in practice: JumpCloud (open directory with HRIS connectors), OneLogin (SCIM 1.1 and 2.0), Ping (PingFederate SCIM plus governance), and Rippling (unified HRIS and identity provider) on the source side; and PropelAuth, Scalekit, and SSOJet (SCIM infrastructure with app-handled revocation) on the application side. Descope and PropelAuth gate SCIM to their higher-tier plans.

Choosing based on your role

If you manage a workforce and its SaaS tools, lead with an identity provider (Okta or Microsoft Entra ID) and turn on HR-driven provisioning from your HRIS. If you build a B2B SaaS product that customers want to deprovision, lead with an application-side platform, with Clerk the recommended default, paired with whatever identity provider each customer brings.

Most teams need both sides working together. The customer's identity provider originates the deprovisioning, and your application-side platform enforces it. Where another application-side platform fits better: WorkOS for a standalone federation layer with first-class group sync and built-in SIEM streaming, Auth0 for an existing Okta or Auth0 estate that needs refresh-token revocation and back-channel logout, Stytch for immediate revocation with a simple free tier, and Descope when FedRAMP High is a hard requirement (with its deferred session revocation noted).

Putting it into practice

Evaluate your current offboarding process

Run your current process against this checklist. Each unchecked box is a gap an auditor or an attacker can find.

Checklist

Questions to ask auth and identity vendors

When you evaluate a platform, ask the questions that separate real offboarding from a checkbox:

  • Do you support inbound SCIM (directory sync), and which identity providers are documented?
  • On a deprovision, do you deactivate the account and revoke active sessions, and how quickly?
  • Is session revocation automatic, or does my code have to call an API?
  • Do you sync group memberships and reclaim licenses or seats?
  • What webhook or event stream fires on a lifecycle change, and is delivery retried?
  • What audit logs do you keep, for how long, and can they stream to my SIEM?
  • What evidence can I hand an auditor to prove a specific user was deprovisioned on a specific date?

A roadmap to automated offboarding

You do not have to automate everything at once. A workable order:

  1. Inventory. List every app and every current manual offboarding step, so you know the real surface area.
  2. Centralize on an identity provider. Put your apps behind one directory and enable SSO, so there is a single place to originate deprovisioning.
  3. Turn on SCIM or directory sync for critical apps. Start with the apps holding the most sensitive data, and confirm each one deactivates the account and revokes sessions.
  4. Automate with webhooks and verify with audit trails. React to lifecycle events in your own systems, then check the audit log to confirm each deprovisioning actually fired.

Common challenges and how to avoid them

A handful of failure modes show up again and again:

  • HR-to-IT ownership gaps. When nobody clearly owns offboarding, HR does the paperwork and assumes IT handled access, and IT assumes HR will tell them. Name an owner and a hand-off.
  • Apps without SCIM support. Plenty of long-tail apps have no SCIM, so keep a documented manual step for them rather than pretending the automation covers everything.
  • Partial deprovisioning. The account gets deactivated but a group membership, a paid license, or a live session survives. This is also where the non-human identity gap lives: standard SCIM deprovisioning does not revoke the API keys, OAuth tokens, SSH keys, or service accounts a departing employee created, and only about 20% of organizations have a formal process to offboard those, while 68% of GitHub tokens carry no expiry (CSA & Astrix, 2024). No authentication platform in this comparison closes that gap on its own — deprovisioning revokes the user's own account and sessions, but none of them automatically finds or revokes the third-party API keys, tokens, or service accounts a departing employee created elsewhere. (Okta surfaces some through its separate identity-governance and posture products, and Microsoft Entra ID treats service-principal and app-registration cleanup as its own owner-driven lifecycle, but neither is part of SCIM user deprovisioning.) Closing it takes dedicated non-human-identity governance or SaaS security posture management (SSPM) tooling run alongside your auth platform, so treat non-human credentials as their own offboarding track.
  • Trusting without verifying. A configured automation is not a confirmed one. Check the audit log after a real termination to prove access was removed, instead of assuming the workflow ran.

This concludes the two-part series on HR-driven offboarding. By combining an identity provider's deprovisioning signal with an application-side platform's enforcement, you can close the gap between an HR termination and access revocation, turning a manual security risk into an automated, auditable control.

Frequently asked questions

How does Clerk handle deprovisioning without SCIM?

For Google Workspace and Microsoft Entra ID, Clerk offers an EASIE connection that checks the upstream account status before issuing a new session token. If the user is suspended or deleted upstream, Clerk revokes their existing sessions automatically within a window of up to 10 minutes, requiring no SCIM configuration.

Why do I need a webhook handler if SCIM deactivates the user?

SCIM deactivates the user account and revokes sessions, but it cannot clean up app-specific resources. A webhook handler listens for the deprovisioning event (like user.updated with a banned status) to trigger your custom logic, such as canceling downstream integrations or writing an internal audit record.

What should an offboarding audit trail include?

An audit trail should provide an immutable record of who was deprovisioned, when it happened, and from where. This evidence is required by compliance frameworks like SOC 2 and HIPAA to prove that access was actually revoked, rather than just intended to be removed.

In this series

  1. HR-driven offboarding for B2B SaaS: SCIM, webhooks, and audit trails
  2. HR-driven offboarding for B2B SaaS: SCIM, webhooks, and audit trails - Part 2 (you are here)