A healthtech account shutdown strategy has to separate profile state and session revocation from the later deletion of clinical, billing, or audit data. Treating those moments as one database operation makes a managed-auth migration look easy in a notebook and dangerous in production.
Short answer: choose staged account shutdown over immediate deletion: freeze profile changes, revoke every application session, block Google and GitHub sign-in, retain only the records required by policy, and let a separate, retryable process perform eventual deletion. Immediate purge is suitable only when no retention, recovery, or audit obligation can apply.
The evaluation constraint is simple: after shutdown begins, no old session or social login may restore access, while an authorized worker must still be able to explain what remains and why. That is the result to test. Deletion latency is secondary.
How should account shutdown coordinate profile state, session revocation, and eventual deletion?
Use one authoritative lifecycle state for the local account, then make authentication and deletion obey it. A practical model has active, shutdown_pending, retained, and deleted states. The exact labels don't matter; the invariants do.
In active, profile edits and new sessions are allowed. Moving to shutdown_pending records the decision, freezes mutable profile fields, increments a session epoch, and denies every new login. A retention decision then moves the account to retained with a deletion eligibility time, or directly queues deletion when policy permits. deleted is terminal for the local account, although independently governed records may remain under their own retention rules.
That separation prevents a subtle failure during migration off a managed provider. Google and GitHub identify an external principal, but the local account decides whether that principal may enter the application. If the callback handler creates or reactivates a profile merely because a provider returned a valid identity, shutdown can be undone by the next social sign-in. The callback must resolve the existing provider link, load the local lifecycle state, and refuse session issuance unless that state is active.
One gate. Everywhere.
Keep the lifecycle check below individual login handlers so password, Google, GitHub, refresh-token, and support-assisted flows cannot drift. During the migration, the old and new authentication paths should consult the same local decision point. This makes dual-running testable without allowing either provider stack to become the source of truth for account status.
The simple delete-first model fails the migration test
The tempting design is a DELETE handler that removes the profile row, clears the browser cookie, and schedules whatever cleanup remains. It passes a happy-path demo. It also combines four decisions that fail differently: whether access ends, whether sessions remain valid, whether data must be retained, and whether physical erasure has completed.
Consider the awkward sequence. A user has two browser sessions and one mobile session. They request shutdown from a browser while a refresh request is already in flight. Their Google identity and GitHub identity both map to the same local account. A delete-first implementation may clear one cookie yet leave two server-side credentials usable; if it removes the provider links first, a later callback can look like a new person and create a fresh account. If cleanup then retries without a stable account identifier, workers may no longer know which derived records belong to the deletion request. None of these are exotic provider behaviors. They are consequences of collapsing access control and erasure into one moment.
The staged design makes the first transaction small and decisive. It changes the lifecycle state, advances a monotonic session epoch, stores the shutdown request time and policy decision, and emits an outbox event in the same local transaction. Session validation compares the credential's epoch with the account's current epoch and also requires active; a mismatch ends access even if a cache or token has time left. The browser can receive a generic signed-out response, but clearing client state is cleanup, not the security boundary.
Don't depend on timing.
OWASP recommends invalidating sessions after reauthentication and other high-risk events, and it warns that session identifiers must be protected throughout their lifecycle. Account shutdown is at least as consequential as a credential change. Apply revocation centrally, require recent authentication before accepting the request, and avoid revealing through the response whether a particular social identity exists.
A focused FastAPI domain example
The useful code is the transition rule, not a vendor-specific callback. This Python example produces the atomic changes an application service should persist. Storage, queues, and token formats stay behind interfaces, which keeps the rule identical while the managed provider is being replaced.
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import StrEnum
class AccountState(StrEnum):
ACTIVE = "active"
SHUTDOWN_PENDING = "shutdown_pending"
RETAINED = "retained"
DELETED = "deleted"
@dataclass(frozen=True)
class Account:
account_id: str
state: AccountState
session_epoch: int
@dataclass(frozen=True)
class ShutdownPlan:
state: AccountState
session_epoch: int
requested_at: datetime
delete_after: datetime
event_name: str = "account.shutdown.requested"
def plan_shutdown(
account: Account,
requested_at: datetime,
retention: timedelta,
) -> ShutdownPlan:
if requested_at.tzinfo is None:
raise ValueError("requested_at must be timezone-aware")
if retention < timedelta(0):
raise ValueError("retention cannot be negative")
if account.state is not AccountState.ACTIVE:
raise ValueError("account is not active")
return ShutdownPlan(
state=AccountState.SHUTDOWN_PENDING,
session_epoch=account.session_epoch + 1,
requested_at=requested_at.astimezone(timezone.utc),
delete_after=requested_at.astimezone(timezone.utc) + retention,
)
def can_issue_session(account: Account, credential_epoch: int) -> bool:
return (
account.state is AccountState.ACTIVE
and credential_epoch == account.session_epoch
)
The service layer should write the returned plan and its outbox record atomically. A worker can then process deletion idempotently: lock the account, verify that delete_after has passed, check for a legal hold or changed retention decision, erase eligible profile fields and provider links, and mark the account deleted. Repeating the worker must produce the same final state. For a healthtech system, the deletion inventory should distinguish authentication profile data from medical, claims, consent, and audit records; “delete the user” is too vague to be an executable policy.
Keep provider unlinking late enough that shutdown retries retain a stable mapping, but never treat an external unlink as session revocation inside the application. Those are different controls. A provider can stop authorizing future grants while an application session created earlier still exists, so the local epoch and lifecycle gate remain necessary.
What to measure before adopting the staged choice
Turn the migration into an eval harness. Feed the same fixtures through the legacy and replacement authentication paths, then assert lifecycle outcomes rather than comparing implementation details. At minimum, test one account linked to both Google and GitHub, three concurrent sessions, an in-flight refresh, duplicate shutdown requests, an early deletion-worker run, and a later retry. A callback after shutdown must never issue a session or create a replacement profile. A stale credential should receive the same unauthenticated treatment regardless of which login path originally issued it.
Measure revocation propagation time, the count of session-issuance attempts blocked by non-active state, deletion jobs by age and terminal outcome, records retained by policy category, and unexplained differences between the two auth paths. Don't put email addresses, provider tokens, or health data into those metrics. Use opaque account and request identifiers so the observability layer doesn't become another deletion surface.
I'm not sure a universal retention interval is defensible across healthtech products; jurisdiction, record type, contracts, and legal holds can change the answer. Resolve that uncertainty with counsel and data owners, then encode the approved policy as versioned input to the shutdown plan. The architecture should support a policy decision without pretending engineers can invent one.
The catch is operational weight. Staged shutdown needs a lifecycle column, centralized session checks, an outbox or equivalent durable handoff, an idempotent worker, and reconciliation. Immediate deletion is the better choice for a genuinely disposable account with no server-side sessions, linked records, recovery window, or retention duty. Likewise, stick with the current managed provider during migration when it is still the only component capable of enumerating and revoking all live sessions; switch authority only after the replacement passes that inventory test.
The decisive preproduction metric is the number of successful authentications after the shutdown transaction commits. It must be zero. Once that invariant holds across both social providers and every legacy session format, eventual deletion can proceed at the pace policy allows without leaving access in limbo.
Top comments (0)