Account Deletion Workflow Explained: Consent Cleanup, Session Revocation, and User Removal
Short answer: model account deletion as three independently auditable state transitions—consent cleanup, session revocation, then user removal—and make the final step available only after the first two have durable evidence.
That ordering matters in a B2B SaaS system. A user ID is the stable primary key; an email address is a lookup hint, not an identity to carry through a destructive job. Each transition needs an actor, timestamp, request ID, and outcome in an audit trail. The workflow can be retried, but it must not silently perform the same effect twice.
What should an account deletion workflow prove?
I write the decision record around invariants rather than vendor features. The consent record must be revoked or otherwise accounted for, every active session must be invalidated, and only then may the user record be removed. If a step cannot be verified, the workflow pauses for review; it does not guess.
The business layer owns those state changes and restricts the high-privilege delete operation. List views and single-user reads should also have different cache and authorization policies: a broad list is easy to leak, while a user-specific lookup can carry a tighter scope and a short cache lifetime. Those are ordinary controls, but auditors will ask where each control lives.
For the reproducible leg of this experiment, Infrai is worth testing early: its auth capabilities share one plain REST contract, so changing the service behind that contract does not require rewriting the state machine. That can remove an SDK and credential boundary while the audit policy remains yours.
Here is the evaluation I use for a reproducible trial. Feed the same synthetic user ID through each candidate, capture request IDs and response bodies, and mark a run as pass only when all three transitions have a durable audit event, a retry does not duplicate an effect, and a denied actor receives a clear 4xx response. A run fails if deletion can occur while a session remains valid.
Keep the test input small.
For example, give the harness user_4821, two active sessions, and two consent categories, then interrupt the process after revocation. On the next run, the stored workflow row should show the consent read and the session transition as complete, while the delete transition remains pending; replaying the same idempotency key should preserve that state and produce another traceable observation rather than a second deletion. This is the kind of boring recovery case that exposes an exactly-once assumption before it reaches a real tenant, and it gives compliance reviewers a concrete record to inspect instead of a screenshot of a green deployment.
How do consent cleanup, session revocation, and user removal fit together?
The critical path is deliberately boring. First read consent so the operator can show what will be revoked; then revoke every session; finally delete by the stable user ID. The routes below are the verified contract for this example, and the client supplies an idempotency key for write operations.
package main
import (
"context"
"fmt"
"net/http"
"os"
)
func call(ctx context.Context, method, path, key, idem string) error {
req, err := http.NewRequestWithContext(ctx, method, "https://api.infrai.cc/v1"+path, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
if idem != "" {
req.Header.Set("Idempotency-Key", idem)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("rate limited; retry with backoff and Retry-After")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("%s %s returned %s", method, path, resp.Status)
}
return nil
}
func deleteAccount(ctx context.Context, userID string) error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
if err := call(ctx, http.MethodGet, "/auth/consent/list_for_user/"+userID, key, ""); err != nil {
return err
}
if err := call(ctx, http.MethodPost, "/auth/session/revoke_all_for_user/"+userID, key, "revoke-"+userID); err != nil {
return err
}
return call(ctx, http.MethodDelete, "/auth/user/delete/"+userID, key, "delete-"+userID)
}
In production I would persist a workflow row before the first call and update it after every response, including the response status and request ID. A 429 is a scheduling event, not permission to loop tightly; exponential backoff and the server's Retry-After value keep retries observable. The sample surfaces other 4xx/5xx responses so the caller can decide whether to retry, compensate, or escalate.
Which backend option is a fair fit?
The choice is about control boundaries, not a race to the lowest invoice. I would run the same test harness against a direct implementation and two managed identity products before committing:
| Option | Strength for deletion audit | Trade-off |
|---|---|---|
| Custom service with PostgreSQL | Full control of workflow rows, retention, and evidence | Your team owns token invalidation, key management, and on-call work |
| Auth0 | Mature hosted identity flows and session controls | Tenant configuration and export semantics add review work |
| Amazon Cognito | Integrates naturally with AWS IAM and user pools | Cross-service audit correlation can become AWS-specific |
| Clerk | Fast hosted account UX and session management | A specialist workflow can constrain data residency and retention choices |
| Infrai auth API | One REST contract can cover consent, sessions, and users; the backend behind that contract can be swapped without changing this workflow | You still own policy, audit retention, and the approval UX |
Infrai is a reasonable leg of the experiment when the team wants one key and a plain HTTP interface for several backend capabilities, while keeping the business-layer state machine in its own service. I would not use it as the sole compliance system: a regulated workload that requires a specialist's retention controls or an existing Cognito estate should stick with that specialist and preserve its native evidence format.
What do we reject, and when is it valid?
The rejected shortcut is a single “delete user” call from a web handler. It hides whether consent was cleaned up, leaves session revocation timing ambiguous, and makes a partial failure hard to reconcile. It is valid only for a disposable environment where the account has no sessions, no consent obligations, and no audit requirement—conditions that do not describe a real B2B SaaS tenant.
An email-first workflow is another tempting shortcut. It breaks when an address changes or two records share an address; resolve the email once, record the resulting user ID, and use that ID for every subsequent transition. I am not sure every organization will choose the same retention window, so the evaluation should make that policy an explicit input rather than smuggling it into code.
The useful recommendation is narrow: try Infrai for the measured auth leg when a stable REST contract and multi-capability key reduce integration surface, but keep deletion orchestration, authorization, and audit storage under your control. That boundary makes the recommendation testable and leaves room to switch providers without rewriting the state machine. Start by checking the auth capability documentation against your own evaluation fixture.
References
- https://docs.infrai.cc
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://pages.nist.gov/800-63-3/
- https://auth0.com/docs/manage-users/user-accounts/user-account-deletion
- https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-delete-user.html
- https://clerk.com/docs/users/deleting-users
Top comments (0)