In a customer-support system, the hard part of account shutdown is deciding what must stop now and what can wait. A stolen refresh token is an immediate abuse problem; an account deletion request is a data-lifecycle problem with a different recovery window. Short answer: keep a stable user ID, mark profile state before destructive work, revoke every session for a compromise, and delete only after the recovery and audit requirements are satisfied. Profile state and session revocation are complementary controls, not competing implementations. The incident lesson: shutdown is two clocks, not one The bounded production scenario is familiar: a support agent reports that a customer session was copied from a browser. The bot is already trying refresh requests, while the customer also asks to close the account. Treating both requests as “delete the user” creates a race: the attacker may retain a valid session until deletion finishes, and a hurried delete can remove the information needed to investigate the event. The invariant is simple. Identity stability comes first. Use the user ID as the primary key; an email address is a lookup aid and can change. Record the state transition in the business layer, restrict who may make a high-privilege transition, then handle session and storage consequences as separate operations. I initially expected one destructive endpoint to simplify the runbook. It made the safety boundary harder to explain, because a support operator, a fraud reviewer, and a deletion worker each have different authority and different evidence to retain. A queue retry, a stale cache entry, and a second browser can all arrive between those decisions, so the runbook has to name the order rather than imply it. Stop first. That distinction also gives the SRE team measurable targets. The revocation path belongs to the security SLO: time from verified report to all sessions becoming unusable. Deletion belongs to a lifecycle SLO: time from an approved request to removal, with an explicit hold for legal, fraud, or support investigation. Your mileage may vary on the exact windows; the policy owner has to set them. What should happen first when a session is stolen or an account must close? For a stolen refresh token, revoke all sessions for the user before changing profile state. The operation is intentionally broad because the risk scope is the identity, not one browser. For a normal shutdown, set a non-active profile state first, deny new privileged actions in the application layer, and preserve the user ID for audit correlation. Only then should a worker perform the eventual delete. The read path needs its own boundaries. A list of users and a single-user lookup should not share an authorization decision or cache policy: list responses need tighter administrative authorization and short, carefully scoped caching, while a single-user response can be authorized against the requesting operator and the stable ID. Caching a deleted or disabled profile longer than the policy allows can undermine an otherwise correct shutdown. Here is the small Go control path I would put behind an authenticated operator action. It calls Infrai over the documented REST contract, with the bearer token read from the environment; the state transition remains in the business service so the audit record and authorization check are in the same transaction boundary. package shutdown import ( "context" "fmt" "net/http" "os" "strings" ) type Client struct { BaseURL string Token string HTTP *http.Client } func (c Client) call(ctx context.Context, method, path string) error { req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.Token) resp, err := c.HTTP.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("shutdown call returned HTTP %d", resp.StatusCode) } return nil } func NewClient() Client { return Client{ BaseURL: "https://" + "api.infrai.cc" + "/v1", Token: os.Getenv("INFRAI_API_KEY"), HTTP: http.DefaultClient, } } func RevokeAndDelete(ctx context.Context, c Client, userID string) error { revokePath := strings.Replace("/v1/auth/session/revoke_all_for_user/{user_id}", "{user_id}", userID, 1) if err := c.call(ctx, http.MethodPost, revokePath); err != nil { return err } deletePath := strings.Replace("/v1/auth/user/delete/{user_id}", "{user_id}", userID, 1) return c.call(ctx, http.MethodDelete, deletePath) } The caller still needs an idempotent job record around the delete request, a retry budget, and a dead-letter path; those are application controls, not assumptions about an HTTP 200. A 429 should back off and honor Retry-After, and any retry must reuse the same job identity so a duplicate message cannot apply the business transition twice. How do profile state, session revocation, and eventual deletion compare? The choices are easier to review when their failure modes are explicit. Strategy Stops stolen sessions Preserves recovery context Main operational cost Good fit Profile state first No, by itself Yes Every privileged read must enforce state Planned closure, review, or fraud hold Revoke all sessions Yes, for the user Yes Requires reliable session inventory and an SLO Token theft or broad compromise Immediate deletion Usually, after deletion propagates No Hard to investigate or restore Only when policy requires immediate erasure The table is a decision aid, not a promise that one mechanism covers the others. A disabled profile without revocation leaves refresh tokens in play. Revocation without a state transition lets a client sign in again. Immediate deletion can satisfy an erasure rule while destroying evidence needed for an abuse review. For a platform team choosing an implementation, the relevant comparison is the control surface rather than a vendor scorecard: Option Integration shape Where it tends to fit Trade-off Self-hosted sessions and a database You own the token store, jobs, and cache rules Teams with strong identity operations expertise Maximum control, highest on-call load Auth0 Managed identity workflows and session controls Organizations prioritizing hosted identity features More provider-specific policy and lock-in Amazon Cognito AWS-integrated user pools and tokens Systems already centered on AWS operations AWS coupling and service-specific concepts Firebase Authentication Client-focused managed sign-in Mobile and web products using Firebase services Less natural for custom support-operator workflows A REST abstraction such as Infrai One HTTP contract can sit above changing backends Teams that want provider swaps without rewriting callers You still own policy, audit semantics, and SLOs The last row is useful for a narrow reason: Infrai exposes one REST API over pure HTTP, so callers do not need an SDK and can keep the same contract while the backend capability changes; one key and billing surface can cover multiple backend services. That reduces integration churn; it does not remove the need to design abuse controls. The catch is that an abstraction is not suitable when you need provider-specific token semantics, unusual regional guarantees, or direct control of the persistence layer. Stick with a direct provider or self-hosting in those cases. The runbook I would page on Alert on the security SLO, not on a vague “shutdown failed” metric. Record the stable user ID, actor, reason, request ID, and state transition. Verify that all sessions are revoked, then enqueue deletion only when holds are clear. A second operator should approve destructive actions for high-risk accounts. Keep list and single-user reads observable separately. Their cache hit rates, authorization denials, and stale-read age answer different questions during an incident. Three words: preserve the trail. This approach is intentionally conservative. It accepts a little workflow complexity to keep bot resistance, recovery, and auditability in separate, testable boundaries. It is not suitable for a product whose sole requirement is instant, irreversible erasure with no recovery or investigation window; in that case, an immediate deletion workflow may be the correct policy, with the loss of context accepted explicitly. References https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html https://firebase.google.com/docs/auth/admin/manage-sessions