Skip to content

Latest commit

 

History

History
63 lines (52 loc) · 25.8 KB

File metadata and controls

63 lines (52 loc) · 25.8 KB

Soft Delete Policy

Chinese version: zh-Hans/soft-delete.md

This document answers one question: after you call a DELETE endpoint in XID, what actually happens to the data, and can it be recovered. The reader is an integrator working against the Management API, and any contributor adding a new deletable resource.

The principle: identity, authorization and audit resources are always soft deleted and filtered out of ordinary read paths. Only one-time credential material and aggregate facts with a retention window are hard deleted. The complete list of hard deletes is in "Hard delete exceptions" at the end of this document.

Delete semantics per resource

Resource Fields Delete semantics
users deleted_at, status Sets deleted_at = now and status = deleted. Management API list, detail, update, delete, ban, unban, bulk metadata and export all filter on both status = deleted and deleted_at IS NOT NULL. POST /v1/users/:id/restore restores a deleted user to active and clears deleted_at, after checking that no active non-deleted user already holds the same username or external_id. Platform user search, stats totalUsers and tenants userCount count active non-deleted users only. The account portal /v1/me, /v1/me/profile and the RBAC metadata helper all require the user to be active with deleted_at IS NULL. Uniqueness of username and external_id constrains active non-deleted users only, so a soft deleted user releases its identifiers
organizations deleted_at, status Sets deleted_at = now and status = deleted. Ordinary list and detail filter deleted. POST /v1/organizations/:id/restore restores active and clears deleted_at
api keys revoked_at A revoked key can no longer authenticate; ordinary list and detail filter revoked. An API key is a secret credential, so there is no restore
sessions status Sets status = revoked. Management API list and detail filter revoked. Reading a cookie session and issuing a session both require the linked user to be active with deleted_at IS NULL, and revoke_all rejects a deleted user
mfa factors status A TOTP factor is set to status = revoked; a backup code factor marks every code in the same batch as used
trusted devices revoked_at GET filters revoked by default
passkey credentials revoked_at DELETE sets revoked_at; sign-in and listing filter revoked
user identities revoked_at Deleting a social connection sets revoked_at; social sign-in, SSO JIT and listing filter revoked
memberships status DELETE sets status = inactive; list and detail default to active. Creating or restoring a membership requires the target user to be active with deleted_at IS NULL. POST /v1/organizations/:orgId/memberships/:membershipId/restore restores active
invitations status DELETE and revoke set status = revoked; list defaults to pending and detail returns pending only. An invitation token is one-time credential material, so there is no restore
applications status Management API DELETE sets status = deleted; OIDC, DCR and token auth accept active only. POST /v1/applications/:id/restore restores active and does not return the client secret
SSO connections status DELETE sets status = deleted; SAML, OIDC RP and HRD accept active only. POST /v1/connections/:id/restore restores active
directories status, deleted_at DELETE sets status = deleted and sync_status = disabled, and clears the previous-token grace fields; SCIM tokens are accepted for active directories only. POST /v1/directories/:id/restore restores active, clears deleted_at, resets sync_status = idle and returns a new SCIM token exactly once
roles status, deleted_at DELETE sets status = deleted; list and detail default to active. POST /v1/roles/:id/restore restores active and clears deleted_at
permissions status, deleted_at DELETE sets status = deleted; list and detail default to active. POST /v1/permissions/:id/restore restores active and clears deleted_at
webhooks status DELETE sets status = deleted; list and detail default to active. POST /v1/webhooks/:id/restore restores active and does not return the signing secret
organization domains status, deleted_at DELETE sets status = deleted; HRD and ordinary listing filter deleted
SCIM users status, deleted_at DELETE sets active = false, status = deleted and deleted_at = now; SCIM list and detail filter deleted
SCIM groups status, deleted_at DELETE clears the group members, then sets status = deleted and deleted_at = now; SCIM list and detail filter deleted
project grants revoked_at, status DELETE and revoke set status = revoked and revoked_at = now, and cascade user_grants.revoked_at for every user grant under the same grant. Ordinary list and detail filter revoked
expired sessions retention expires_at The hourly cron hard deletes session rows with expires_at < now through hardDeleteExpiredSessions; active and revoked states are still expressed by sessions.status
usage monthly retention year_month The daily cron hard deletes aggregate metering facts older than the 13-month retention window through hardDeleteOldMonthlyUsage; identity, audit and authorization resources never take this path

Query filtering rules

  • Ordinary users, ordinary organizations, the account portal, the platform console, the Management API and the RBAC token metadata helper never return or count resources with deleted_at IS NOT NULL, revoked_at IS NOT NULL or status = deleted by default.
  • The username and external_id duplicate checks in Management API POST /v1/users and PATCH /v1/users/:id match active non-deleted users only. The D1 unique indexes users_tenant_username_unq and users_tenant_external_id_unq are active-only partial unique indexes, so a soft deleted user no longer occupies either identifier.
  • Every authentication entry point that issues a cookie session goes through issueSession, which checks that the linked user is active with deleted_at IS NULL. That covers password, Magic Link, OTP, passwordless, passkey, social OAuth, SAML/SSO and the automatic sign-in after a password reset.
  • When platform administration needs to see deleted or revoked resources, it must go through the platform management endpoints, gated by instance_manager.
  • The tenant isolation predicate must still be injected by createTenantDb. Soft delete filtering may only append a narrowing condition; it can never replace the tenant_id condition.
  • TenantScoped exposes no generic delete. A hard delete requires an explicit hardDelete call. Deleting a business resource defaults to a status field or a timestamp field.
  • Every restore endpoint has a cross-tenant authorization test: a cross-tenant restore returns 404 and modifies no row of the victim tenant, secrets are not echoed back, a users restore that hits an active identifier conflict returns 409, and a membership restore rejects a deleted user. The tests live in apps/server/worker/v1/__tests__/isolation.test.ts.

Hard delete exceptions

Resource Call site Reason Boundary
verification tokens auth/otp.ts, auth/magic-link.ts, me-auth/email-verify-token.ts, me-auth/passwordless.ts, me-auth/mfa-challenge.ts One-time authentication tokens. Once verified, expired, or past the failure limit, no replayable material may be kept Deletes only through the current tenant query layer and by token purpose
password reset tokens me-auth/password-reset.ts A password reset token is one-time secret material: issuing a new token invalidates the old one, and verification kills it at once Deletes only through the current tenant query layer and by token hash
backup codes auth/backup-codes.ts Regenerating recovery codes invalidates the previous batch; recovery codes are not retained as audit resources, the audit event is written separately Deletes only through the current tenant query layer and by user id
SCIM group members scim/groups.ts, scim/shared.ts The membership join table expresses the current group member set, which must be synchronized on group delete or on PATCH replace/remove The group and the directory user remain protected by soft delete and tenant-scoped queries
SCIM pending members scim/users.ts, scim/shared.ts Unknown-member placeholders exist only for SCIM idempotency compensation and should be cleared once the member resolves or the group membership is recomputed Clears only the placeholder for the matching group/ref in the current tenant
expired sessions crons/hourly.ts hardDeleteExpiredSessions The sessions table has a retention window, and an expired row can no longer take part in authentication, revocation or restore Deletes only expires_at IS NOT NULL AND expires_at < now
monthly usage retention crons/daily.ts hardDeleteOldMonthlyUsage Aggregate metering facts keep a 13-month window only, and past that they are not identity audit resources Deletes only usage_monthly rows with year_month < cutoff
Durable Object transient state durable-objects/* OAuth state, PAR, device flow, challenge, rate limit and metering sets are short-lived coordination state Never an entry point for restoring a D1 business resource

Migration rules

  • Only add migrations. Never rewrite an existing baseline.
  • A new soft delete field is named revoked_at or deleted_at, typed as a Unix millisecond timestamp.
  • After adding the field, update the schema, the route filters, the write endpoints and the tests together.