Short answer: make deletion a two-phase, abuse-resistant workflow that proves user intent, revokes every session first, records consent changes, and only then erases the account behind an idempotent job.
In a customer-support system, “delete my account” is a security-sensitive operation disguised as a settings button. A successful attacker can use it to destroy a victim’s ticket history, or use a bot farm to turn support agents into an expensive deletion service. The design target is therefore not a fast DELETE; it is a bounded state transition with an auditable decision and a measurable SLO.
What should a customer-support deletion workflow protect?
Start by separating identity proof from execution. A signed-in browser session is useful context, but it is not sufficient proof for an irreversible request. Require recent re-authentication or a verified out-of-band challenge, apply rate limits per account and network, and make the confirmation token single-use with a short lifetime. OWASP’s Authentication Cheat Sheet recommends reauthentication for sensitive actions; that is the right baseline here.
The abuse signal is usually a pattern, not one request: repeated deletion challenges, many accounts from one IP range, impossible location changes, or a sudden burst after a credential-stuffing event. Put these signals in a policy decision point that can hold a request for manual review. Do not reveal which signal fired. A generic response such as “Your request is being processed” prevents account enumeration while preserving a clean support experience.
Keep the deletion request separate from the user row. The request should contain an opaque request ID, subject ID, policy version, actor type, challenge timestamp, and an expiry. Store a hash of the confirmation token rather than the token itself. This gives incident responders something to inspect without creating a second credential database.
How do consent cleanup, session revocation, and user removal fit together?
Treat the workflow as an ordered state machine:
-
requested: the user has passed the initial checks, but nothing is erased. -
confirmed: a fresh challenge proves intent; enqueue one request ID. -
revoked: invalidate access and refresh sessions, API tokens, password-reset tokens, and remembered devices. -
consent_cleared: withdraw optional consent records and stop downstream processing. -
purging: delete or irreversibly anonymize user-owned data according to retention rules. -
complete: emit a receipt containing the request ID and completion time, not personal data.
The order matters. Revocation closes the door while the purge runs. Consent cleanup is a separate record operation because a legal hold or a narrowly defined accounting retention period can make some records ineligible for deletion; “delete everything” is not a safe implementation of a retention policy. Keep a tombstone with a salted subject hash and the request ID so a retry cannot recreate an identity accidentally.
Here is the core boundary in Go. The repository and queue are deliberately generic: the important contract is idempotency and the fact that revocation happens before destructive work.
package deletion
import (
"context"
"errors"
)
var ErrAlreadyHandled = errors.New("deletion request already handled")
type Store interface {
LoadRequest(context.Context, string) (Request, error)
MarkConfirmed(context.Context, string) error
RevokeSessions(context.Context, string) error
ClearOptionalConsent(context.Context, string) error
EnqueuePurge(context.Context, string) error
}
type Request struct {
ID string
SubjectID string
State string
}
func Confirm(ctx context.Context, store Store, requestID string) error {
r, err := store.LoadRequest(ctx, requestID)
if err != nil {
return err
}
if r.State != "requested" {
return ErrAlreadyHandled
}
if err := store.MarkConfirmed(ctx, r.ID); err != nil {
return err
}
if err := store.RevokeSessions(ctx, r.SubjectID); err != nil {
return err
}
if err := store.ClearOptionalConsent(ctx, r.SubjectID); err != nil {
return err
}
return store.EnqueuePurge(ctx, r.ID)
}
The worker must also be idempotent. A queue redelivery should observe purging or complete and exit safely, while a partial failure should leave a durable state and an alert. Never put the email address, ticket text, or access token in queue payloads or logs. Pass the request ID and fetch data under a service identity with the smallest useful scope.
Where does a buy-vs-build decision change the risk?
The component choice is secondary to the contract. A managed identity service can shorten the path to reauthentication and token revocation, while a self-hosted stack gives direct control over data residency and retention. Either choice still needs your deletion state machine, abuse policy, and evidence trail.
| Choice | Strength | Cost or boundary | Suitable when |
|---|---|---|---|
| Managed identity service | Mature challenge flows and key rotation | External dependency and provider-specific retention semantics | A small team needs a standard login surface quickly |
| Self-hosted identity store | Full control of records and deletion timing | Your team owns patching, recovery, and on-call | Residency rules or bespoke session models dominate |
| Hybrid boundary | Identity provider handles authentication; your service owns deletion orchestration | Two failure domains and more integration tests | Support data and identity data have different owners |
Capacity planning belongs in this decision. Estimate peak deletion requests, queue retry volume, and the number of session rows per subject; then reserve worker capacity for a retry storm, not just the average day. I would set separate SLOs for challenge completion, revocation visibility, and purge completion because one aggregate “deletion latency” hides the dangerous interval where a user is still authenticated.
The catch is that a managed component is not suitable when its audit export cannot satisfy your regulator’s retention boundary, and a self-hosted component is a poor fit when nobody can staff emergency patching. Stick with the boundary your on-call rotation can actually operate.
How do you verify deletion without leaking a surviving identity?
Verification should test both positive and negative paths. In a staging environment, create a subject with two browser sessions, one mobile refresh token, optional marketing consent, support tickets, and an active export job. Confirm that every credential is rejected after the revoked transition, that optional consent is no longer eligible for use, and that the purge worker can be retried without duplicating events. Then test a challenge replay, an expired token, two concurrent confirmations, and a request that hits a legal hold.
Production checks should be aggregate and privacy-preserving: count requests by state, age, and policy version; alert on revocation lag against its SLO; and sample tombstone integrity without displaying identifiers. A deletion receipt can go to the requester’s verified channel, but it should contain only the request ID, completion timestamp, and support contact path.
Rollback needs a narrow definition. You can stop queued purge work before it starts, or restore data from an encrypted backup under an approved incident process. You cannot “undo” a consent withdrawal safely by replaying an old event, and a completed erasure should remain completed. Document that asymmetry in the runbook so an operator does not improvise a resurrection endpoint.
Three words: revoke first, purge later.
Top comments (0)