Skip to content

B2C Account Deletion & Pause

Self-service B2C users (learner or trainer, role == b2c_user) get an Account tab in Settings with two actions: Pause (reversible deactivate) and Delete (DPDP-grade erasure). Before this, the only option was admin-driven deactivation, which kept all PII in the database forever.

How it works

Delete picks a reason, confirms, anonymizes PII immediately, keeps a 30-day recovery grace window, then a background job hard-purges child data. The chatbot's cross-cloud long-term memory is erased via a signed S2S call.

State model:

  active ──pause──▶ paused ──login/reactivate──▶ active
    │                 │
    │                 └──(stays until user returns; reversible)
    └──delete(reason)──▶ pending_deletion ──+30d──▶ purged
                              └──reactivate within 30d──▶ active (PII already scrubbed*)

Invariants:

  • paused: is_active=false, paused_at set, deleted_at null. PII intact. Login reactivates.
  • pending_deletion: is_active=false, deleted_at set. PII already anonymized. Login is blocked with a "restore until " path. Reason already captured.
  • purged: user row + child PII hard-deleted by background job. Financial records retained with anonymized FK. Deletion-reason feedback row retained (no PII).

* Recovery within grace restores access and account structure, but anonymized identity fields are not un-scrubbed. Recovery means "your account works again," not "your old email/name is back." The delete-confirm copy says so.

Flow rules:

  • Delete: modal asks "Why are you leaving?" (single-select reason + required free text for Other), then a confirm step with warning copy + password entry (or type-to-confirm for OAuth-only accounts with no password) + a destructive confirm. On confirm the backend stores the reason, anonymizes PII, sets deleted_at, fires the chatbot erasure call, then the client logs out to a "scheduled for deletion, restore until " screen.
  • Pause: confirm modal, backend sets paused_at + is_active=false, client logs out. Next login detects paused and reactivates.
  • The Account tab is gated on role === b2c_user; backend endpoints reject non-B2C with 403.
  • Financial/payment/invoice records are retained (user link anonymized) for Indian tax/accounting retention, never deleted.
  • The chatbot erasure call (POST /v1/memory/erase, signed with the existing S2S HMAC) is best-effort and behind CHATBOT_ERASURE_ENABLED. Failures are logged and retried, never block local deletion.

Deletion reasons (single-select)

reason_code Label (en)
not_using I'm not using Kwilo enough
found_alternative I found a better alternative
too_expensive It's too expensive
missing_features Missing features I need
privacy_concerns I have privacy or data concerns
created_by_mistake I created this account by mistake / duplicate account
temporary_account Just a temporary account, no longer needed
other Other (free text, required)

Reasons are stored in account_deletion_feedback (reason_code, reason_text, role, was_b2c, plan_at_deletion, created_at) with an anonymized/nullable user link so the row survives the purge.

API endpoints

Method Endpoint Body Purpose
POST /api/v1/users/me/account/pause none Set paused_at, is_active=false
POST /api/v1/users/me/account/reactivate none Clear paused_at, is_active=true (also via login)
POST /api/v1/users/me/account/delete { reason_code, reason_text?, password? } Store reason, anonymize PII, set deleted_at, fire chatbot erasure

All three require CurrentUser and reject non-b2c_user with 403. delete requires password when password_hash is set; otherwise type-to-confirm is enforced client-side. Reason other requires non-empty reason_text.

Where it lives

  • Route: /settings (Account tab).
  • Backend: apps/backend/src/services/account_lifecycle.py, apps/backend/src/models/account_deletion_feedback.py, /me/account/* routes in apps/backend/src/api/v1/users.py, login handling in apps/backend/src/api/v1/auth.py.
  • Web: apps/web/src/pages/shared/SettingsPage/components/AccountTab.tsx, apps/web/src/constants/accountDeletion.ts.