Short answer: treat account deletion as an ordered, retryable workflow: capture verified consent withdrawal, revoke every active session, then remove or de-identify user records according to retention rules. The order matters because deleting a row first can leave a live token attached to a patient identity.
In a healthtech system, “delete my account” is not one SQL statement. A device fingerprint may be copied into a risk feature store, a refresh token may sit in a browser, and an audit event may need to remain for a statutory period. The user sees one button; the system has several clocks.
I build RAG and agent features in Python, so I test this kind of workflow like a small state machine before wiring it to production. My first instinct was a synchronous endpoint that called three services and returned 204. The first timeout made that design uncomfortable: the identity service had completed, the session service had not, and a retry could not tell whether “remove” had already happened. Idempotency became the design constraint, not an optional polish item.
What should happen before a healthtech identity is removed?
Start by authenticating the deletion request at a level appropriate to the account. A recently re-authenticated user can confirm in the current session; a stale session should trigger step-up authentication. OWASP recommends reauthentication for sensitive account changes, and deletion belongs in that category. Do not accept an email link as the only proof when the link itself is the credential being revoked.
Create a deletion job with a stable identifier and record the requested data scope. “All data” needs a precise interpretation: profile and clinical application data may be erasable, while a billing ledger or safety audit may be retained under a documented legal basis. Mark each class as erase, anonymize, or retain, and store the policy version used to make that choice.
Consent cleanup is its own operation. Withdraw marketing and research consent immediately, stop new downstream exports, and send the withdrawal event to every system that holds a copy. Do not silently rewrite history. Keep an immutable event saying that consent was withdrawn, with the minimum identifiers needed for audit; remove the person-readable payload when the retention window ends.
Then revoke sessions. Invalidate refresh tokens server-side, expire sessions in the session store, and rotate any per-user signing or encryption material that would otherwise let an old credential work. Access tokens that are already issued may be self-contained, so pair short expiries with an introspection or deny-list check for high-risk resources. A successful database delete is not proof that a bearer token is dead.
Only after those gates pass should the worker erase or de-identify user records. Keep the job state separate from the user row so a missing row means “already removed,” not “the job never ran.” That distinction makes retries safe.
How can consent cleanup, session revocation, and user removal stay retryable?
Model each stage as an idempotent command. Every command receives the same deletion job ID, writes an outcome, and can be run again without creating a second side effect. A queue with at-least-once delivery is fine when handlers are designed for duplicates. Exactly-once delivery is an expensive illusion at service boundaries.
Here is a compact orchestration sketch. The adapters are deliberately boring; their contracts are what deserve tests.
from dataclasses import dataclass
from enum import Enum
class Stage(str, Enum):
CONSENT = "consent"
SESSIONS = "sessions"
RECORDS = "records"
@dataclass(frozen=True)
class DeletionJob:
job_id: str
subject_id: str
policy_version: str
def run_deletion(job: DeletionJob, consent, sessions, records, ledger) -> None:
"""Run stages in order; each adapter must be safe to call repeatedly."""
ledger.start_if_absent(job.job_id, job.subject_id, job.policy_version)
if not ledger.done(job.job_id, Stage.CONSENT):
consent.withdraw_all(job.subject_id, idempotency_key=job.job_id)
ledger.mark_done(job.job_id, Stage.CONSENT)
if not ledger.done(job.job_id, Stage.SESSIONS):
sessions.revoke_subject(job.subject_id, idempotency_key=job.job_id)
ledger.mark_done(job.job_id, Stage.SESSIONS)
if not ledger.done(job.job_id, Stage.RECORDS):
records.erase_or_anonymize(job.subject_id, idempotency_key=job.job_id)
ledger.mark_done(job.job_id, Stage.RECORDS)
The ledger should distinguish started, done, and failed with an error category safe for operators to read. Never put a diagnosis, access token, or full device fingerprint in a queue payload. Emit a correlation ID instead, and keep sensitive values in the service that owns them.
My evaluation harness checks more than a happy path. It submits the same job twice, kills the worker after each adapter call, delivers stages out of order, and runs a request for an already-erased subject. The expected result is stable: no consent is re-granted, no session is resurrected, and no second deletion ticket is opened. I also assert that logs contain the job ID but not the subject's email or raw fingerprint.
Where do deletion workflows usually fail?
The most common failure is an incomplete inventory. Teams remove the primary user row and forget analytics tables, notification preferences, device-risk embeddings, or a partner export. Build a data map from actual writes and retention owners, not from the ORM model alone. A quarterly “create test user, delete test user, search every store” exercise catches drift that code review misses.
Another failure is treating revocation as a UI concern. Logging out in one browser does not revoke a phone session, a support portal session, or a refresh token stored by a native app. Keep a subject-level revocation timestamp or token version, and make every sensitive authorization path consult it.
Race conditions are quieter. A background job can write a new preference between consent withdrawal and record cleanup. Use a deletion barrier: once a subject enters deleting, writes for erasable classes are rejected or redirected to a tombstone. This is a small rule with a large payoff.
Here is the sequence I make explicit in tests: request 8f2 is verified at 09:00, the consent service records withdrawal at 09:00:01, and the mobile client submits a profile update at 09:00:02 because it was offline. The write API sees the deleting marker and returns a documented conflict without storing the payload. At 09:00:03 the session worker revokes the refresh-token family; at 09:00:04 the record worker removes the profile and replaces the device-risk feature with an aggregate that cannot identify a person. A retry at 09:05 reads the ledger, skips completed stages, and reaches the same terminal state. Without that barrier and ledger, the late profile write can survive a seemingly successful deletion while operators have no event tying it back to job 8f2.
Then stop.
Finally, measure completion honestly. Track time from verified request to each stage, retry counts, downstream acknowledgement, and the number of records found by the post-delete sweep. A green HTTP response only says the request was accepted. It does not say the obligation is complete.
That distinction is easy to miss.
Choosing guarantees without overbuilding
There is a real trade-off between immediate erasure and a short grace period. Immediate work reduces exposure but increases the chance that a mistaken request is irreversible. A 24-hour, re-authenticated cancellation window can help consumer products, yet it may be unsuitable for a policy that promises prompt withdrawal. Document the choice in the product's privacy notice and make the timer visible to support staff.
| Decision | Stronger guarantee | Cost or limitation |
|---|---|---|
| Synchronous deletion | Caller gets a final result | Poor fit for many stores and transient outages |
| Queued deletion job | Retries and audit trail | User needs status and a completion notification |
| Token deny-list | Fast revocation of issued tokens | Operational storage and lookup on protected requests |
| Short token lifetime only | Simple infrastructure | Not suitable for high-risk actions after a deletion request |
| Full erasure everywhere | Lowest residual identity data | May conflict with legal retention or safety audit duties |
Do not promise a universal “delete everything now” button when retention law says otherwise. Conversely, do not use retention as an excuse to keep marketing consent or an active session. The right boundary is explicit, reviewable, and tested.
A practical readiness check
Before shipping, I want five artifacts: a data inventory with owners, a policy table for erase/anonymize/retain, an idempotent job ledger, a revocation test that covers every client type, and a post-delete sweep report. Run the suite in CI with fault injection, then run a redacted version against a staging environment containing realistic device-fingerprint shapes.
Your mileage may vary on the exact retention periods; those depend on jurisdiction and clinical obligations. The engineering invariant is less ambiguous: once a verified request enters the deleting state, no new erasable data should be accepted, old sessions should lose authority, and retries should converge on the same final state.
Top comments (0)