1
Request
The signed-in user initiates deletion. The server mints a single-use confirmation token (1-hour TTL), sends a confirmation email, and returns HTTP 202. The account stays in
active state until the link is clicked — re-issuing the request inside the TTL re-uses the same token (idempotent).2
Confirm via email link
Clicking the link transitions
auth_user.deletion_state from active to pending_deletion, stamps pending_deletion_at, revokes every API key in the tenant, drops every active session, and emails a cancellation link valid for the full grace window.3
Grace window (30 days by default)
The user can cancel at any point during the grace period. Two cancel paths: clicking the cancel link from the pending-deletion email (or from the email sent when they attempt to sign in), or
POST /auth/account/delete/cancel from a re-authenticated session.4
Permanent purge
When
pending_deletion_at + grace_days < now(), the PendingDeletePurger background sweep cascades: tears down the tenant’s connection oauth tokens, leased tokens, inbound webhooks, edges, items (which cascade metadata + versions), blobs, API keys, outbound webhooks, tenant quotas, in-flight auth_verification tokens, the users row, the tenants row, then redacts the user’s audit trail and deletes the auth_user row.What the user sees
The CLI verb is initiation-only by design. Confirmation and cancellation are email-link flows so the user always has to prove control of the inbox at the consequential moments.
Sign-in-during-grace behavior
The deletion-guard middleware intercepts sign-in attempts against a pending-deletion account before Better Auth’s credential check completes. The response is a generic401 invalid_credentials — the same shape returned for a wrong password — so the surface deliberately does not enumerate which accounts are scheduled for deletion. Two side effects ride the interception:
- A
auth.account.sign_in_blocked_pending_deletionaudit row is written. - A security-notice email is dispatched to the account’s address with copy along the lines of “Someone tried to sign in to your Marfa account. It is scheduled for deletion — click the link below to cancel.” The link is the same single-use cancel URL emitted on initial confirm.
MARFA_ACCOUNT_DELETE_CANCEL_COOLDOWN_MS — see Configuration.
What gets deleted
Every artefact scoped to the user’s tenant:- All items, regardless of state (including trashed)
- All metadata, item versions, edges
- All blobs (per-tenant rows; the content-addressed file on the storage backend is removed when its last metadata row goes)
- Every API key for the tenant
- Outbound and inbound webhooks
- Connection OAuth tokens and leased tokens
- Per-tenant quota overrides
- The
usersprofile row and thetenantsrow itself - The
auth_userrow, which cascades to dropauth_session,auth_account, andauth_passkeyrows
pending_deletion for the next purger tick to retry.
What’s preserved (audit redaction)
Theaudit_log table is not truncated. Existing rows tied to the deleted account stay in place, but their details JSON is rewritten to a sentinel:
action, resource_id, tenant_id, and timestamp survive. This preserves the forensic chain — operators querying who did what when can still see the action sequence — without retaining personally identifying payload. The chain remains queryable by the hashed-id sentinel if a future investigation needs to correlate redacted rows across actions.
The audit rows specific to the deletion lifecycle:
auth.grant.created / auth.grant.revoked (OAuth grant lifecycle) are tracked separately — see Authentication.
Operator perspective
During the grace window, the account is visible to platform-admin tooling as a regular tenant inpending_deletion:
my platform tenants show <id>returns the tenant row.my platform tenants metrics <id>still returns counts (the data is still there until the purger runs).- The tenant’s
auth_user.deletion_stateispending_deletion; sign-in attempts on the account return a generic401(the same shape as wrong credentials) and trigger a security-notice email subject to the per-account cooldown.
my platform tenants show <id>returns 404.- Items, blobs, keys all return 404 to any reader.
- The audit trail remains, with redacted
detailspayloads.
pending_deletion_at + grace_days has elapsed. To force the next sweep without waiting for the 1-hour cadence (e.g. an account just passed its grace window and the operator wants the cleanup now), use my platform account-deletion purge-now. Operator read of pending tokens for support-driven recovery is a separate deferred surface and not part of this build.
Configuration
The purger runs cluster-coordinated: an outer named lock (
account-deletion-purge) keeps one instance running the sweep at any time; a per-account inner lock (account-delete:<auth_user_id>) prevents a cancel from racing the cascade.
Related
- Self-hosting → Platform — operator surfaces for managing tenants. Account deletion is user-initiated, not admin-initiated; the platform commands do not include a “delete tenant” verb.
- Self-hosting → Hosted mode — the auth model this lifecycle plugs into.