Olympus Docs
CookbookData & compliance

Automated deprovisioning workflows

Cleanup when users leave or become inactive

When users leave (B2B employee termination, account abandonment), you need to deprovision: revoke access, remove from groups, clean up resources.

Triggers

Trigger A: SCIM signal

Customer's IdP (Okta, Entra) calls your SCIM endpoint:

PATCH /scim/v2/Users/abc-123
{ "Operations": [{ "op": "replace", "path": "active", "value": false }] }

Your handler deactivates:

await kratos.adminPatch(identityId, [
  { op: "replace", path: "/state", value: "inactive" }
]);
await revokeAllSessions(identityId);
await sendDeprovisionedEmail(identity);

See SCIM endpoint.

Trigger B: Manual admin action

In Athena:

<Button onClick={() => deprovision(identityId)}>Deactivate</Button>

Server action runs same flow.

Trigger C: Inactivity policy

Cron: users inactive 12 months → deactivate.

SELECT id FROM identities
WHERE state = 'active'
  AND (
    (SELECT MAX(created_at) FROM security_audit WHERE identity_id = identities.id AND event_type = 'login') 
    < NOW() - INTERVAL '12 months'
  );

Email warning first ("Your account will be deactivated in 7 days due to inactivity"). If no response, deactivate.

Trigger D: Subscription cancelled

If subscription ends and grace expires:

// In Stripe webhook
if (event.type === "customer.subscription.deleted") {
  const customer = event.data.object.customer;
  await markCustomerInactive(customer);  // grace 30d
}

After grace: deactivate.

What deprovisioning does

For each affected user:

  1. Set state to inactive.
  2. Revoke all sessions (so they're logged out everywhere).
  3. Revoke active OAuth2 tokens (refresh, access).
  4. Remove from teams / orgs (if applicable).
  5. Disable API keys / PATs.
  6. Notify the user.

Optional:

  • Schedule data deletion (per retention).
  • Notify their managers / collaborators.
  • Cancel subscriptions.
async function fullDeprovision(identityId) {
  await db.transaction(async (tx) => {
    await tx`UPDATE identities SET state = 'inactive' WHERE id = ${identityId}`;
    await tx`UPDATE kratos.sessions SET revoked_at = NOW() WHERE identity_id = ${identityId}`;
    await tx`DELETE FROM personal_access_tokens WHERE identity_id = ${identityId}`;
    await tx`DELETE FROM memberships WHERE identity_id = ${identityId}`;
  });
  
  // Stripe
  if (user.stripe_customer_id) {
    const subs = await stripe.subscriptions.list({ customer: user.stripe_customer_id });
    for (const sub of subs.data) await stripe.subscriptions.cancel(sub.id);
  }
  
  // Audit
  await audit.log({ event: "user_deprovisioned", target: identityId, actor: "system" });
  
  // Notify
  await email.send(user.email, "Your account has been deactivated", ...);
}

What stays

After deprovisioning, retain:

  • Identity row (state=inactive, data preserved).
  • Audit log entries.
  • Their owned resources (for billing / accountability).

Their data isn't deleted by deprovisioning. That's a separate user-action ("delete my account").

Reactivation

If user comes back (false termination, returning customer):

async function reactivate(identityId) {
  await kratos.adminPatch(identityId, [
    { op: "replace", path: "/state", value: "active" }
  ]);
  await sendEmail(user.email, "Welcome back", ...);
}

State changes back. Data was preserved.

Resource ownership transfer

If user owned shared resources (documents, projects), what happens when they leave?

Options:

  • Keep: resources stay owned by inactive user. Others can still access if invited.
  • Transfer: bulk-assign to a designated owner (manager, team lead).
  • Delete: aggressive, usually wrong.

For most B2B: transfer to manager. Document the policy.

async function transferOwnership(fromUserId, toUserId) {
  await db`UPDATE projects SET owner_id = ${toUserId} WHERE owner_id = ${fromUserId}`;
  await db`UPDATE documents SET owner_id = ${toUserId} WHERE owner_id = ${fromUserId}`;
  // notify new owner
}

Notification timing

When user is deprovisioned:

  • Immediate notice ("Your account has been deactivated as of [time]").
  • 30 days before scheduled delete (if applicable).

Don't bury the lede. Be transparent.

Audit

Every deprovisioning event:

INSERT INTO security_audit (event_type, target_id, actor_id, metadata)
VALUES (
  'user_deprovisioned',
  $user_id,
  $actor_or_system_id,
  '{"reason": "$reason"}'
);

Reason categories:

  • inactivity_12mo
  • scim_active_false
  • admin_manual
  • subscription_cancelled
  • user_requested

Reportable for compliance.

Bulk deprovisioning

End of a quarter, deprovision lots:

const inactive = await findInactiveUsers();  // some criteria
await processBulk(inactive, deprovision, 5);  // 5 concurrent

Send summary email to admins:

This quarter:
- Deprovisioned 234 users (inactivity).
- 12 users requested reactivation.
- 5 users had ongoing subscriptions cancelled.

Reversibility

Always reversible for 30+ days. Deactivation is soft. Data preserved.

Hard delete is a separate, slower, explicit step.

On this page