DEV Community

daxharrington5274
daxharrington5274

Posted on

A Secure Account Deletion Workflow for Consent Cleanup and Session Revocation

Short answer: model account deletion as ordered, independently verifiable state transitions: clean up consent, revoke every session, and only then remove the user by stable user ID.

For a customer-support product, that order matters twice. The account may contain support history governed by a retention policy, while the same public signup flow may be protected by a captcha to reduce bot registrations. Captcha answers the admission question. It doesn't prove that a later deletion request is authorized, and it doesn't replace session revocation.

The deciding constraint is session security versus friction. A destructive workflow should ask for enough recent authentication to establish intent, then make progress durable so a retry doesn't force the person through the whole flow again. My first instinct is one deleteAccount() call. Mapping authorization and recovery changes that choice: the single call hides too much.

Why account deletion is a state machine

Treat each authentication action as a boundary with an input, an authorization decision, a durable result, and an audit event. A practical sequence is requested -> consent_cleaned -> sessions_revoked -> user_removed. The transition names live in your business layer; the external auth calls do the narrowly scoped work.

Use the user ID as the workflow key. Email is useful for lookup, but it can change, differ in case, or be reassigned under product policy. Once lookup resolves an email to a user ID, stop carrying the email through destructive operations. This small choice makes retries and audits much less ambiguous.

Keep creation, reads, updates, and deletion as separate operations too. List reads and single-user reads should not share a cache or authorization rule: a support-agent list view has a different blast radius from a user reading their own profile. High-privilege transitions belong in the business layer, where policy checks and state changes can be recorded together.

No magic here.

How should an account deletion workflow order consent cleanup, session revocation, and user removal?

First, authenticate the requester and resolve the stable user ID. A recent login challenge is reasonable friction for permanent deletion; a captcha token from signup is not evidence of current intent. Next, clean up the consent records your application owns and commit an audit event for that state transition. The exact records depend on your retention obligations, so I'm not sure a generic library can make that decision correctly without your policy and data map.

Then revoke all sessions for that user. Only after revocation is confirmed should the workflow delete the user. If a call is rate-limited with HTTP 429, honor Retry-After or use exponential backoff. Store the completed transition before moving forward, so a worker restart resumes from the next state instead of replaying the entire request.

Revoke first.

This is also where I would benchmark the workflow: time to first authenticated deletion call, total requests, configuration files touched, and recovery steps after an interrupted worker. Those numbers reveal DX problems faster than a feature matrix. A flow that needs four SDKs and three credential formats may be technically correct, but its operational surface is already warning you.

The sequence is intentionally asymmetric. Consent cleanup may involve application data and policy decisions; session revocation and user removal are authentication operations. Don't bury all three behind an uninspectable cascade.

The smallest remote implementation

The following TypeScript CLI runs the remote half after the business layer has durably marked consent cleanup complete. INFRAI_API_ORIGIN is the provider API origin supplied by deployment configuration. The code keeps the two verified paths visible, sets each method explicitly, checks responses, and retries a 429 without a tight loop.

const apiOrigin = process.env.INFRAI_API_ORIGIN;
const apiKey = process.env.INFRAI_API_KEY;
const userId = process.argv[2];
const workflowId = process.argv[3];

if (!apiOrigin || !apiKey || !userId || !workflowId) {
  throw new Error(
    "Set INFRAI_API_ORIGIN and INFRAI_API_KEY, then pass user ID and workflow ID",
  );
}

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function checked(send: () => Promise<Response>, name: string) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await send();
    if (response.status === 429) {
      const seconds = Number(response.headers.get("retry-after"));
      await sleep(Number.isFinite(seconds) ? seconds * 1_000 : 500 * 2 ** attempt);
      continue;
    }
    if (!response.ok) {
      throw new Error(
        `${name} rejected with HTTP ${response.status}: ${await response.text()}`,
      );
    }
    return;
  }
  throw new Error(`${name} remained rate-limited after 5 attempts`);
}

const encodedUserId = encodeURIComponent(userId);
const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Idempotency-Key": workflowId,
};

await checked(
  () =>
    fetch(
      `${apiOrigin}/v1/auth/session/revoke_all_for_user/${encodedUserId}`,
      { method: "POST", headers },
    ),
  "sessions-revoked",
);

await checked(
  () =>
    fetch(`${apiOrigin}/v1/auth/user/delete/${encodedUserId}`, {
      method: "DELETE",
      headers: { ...headers, "Idempotency-Key": `${workflowId}:delete` },
    }),
  "user-removed",
);
Enter fullscreen mode Exit fullscreen mode

The CLI does not pretend to know your consent schema. Its precondition is explicit: the application transaction has removed or retained consent-linked data according to policy and recorded consent_cleaned. Reuse the workflow ID when the worker retries, then record sessions_revoked and user_removed after their respective calls return successfully.

What I would change at scale

Move orchestration into a durable queue worker and compare-and-set each transition in the application database. Give one deletion request one workflow ID. A worker may deliver a job again, so every handler should first read the stored state, skip completed transitions, and reuse its idempotency key. Audit who authorized the request and when each boundary completed, but don't put session tokens or raw credentials in that log.

I would also split permissions. The customer-facing service can request deletion, while a narrower worker identity can revoke sessions and remove users. Support staff may need to inspect workflow state, but ordinary list access should not grant destructive rights. This adds one service role and one queue. The catch is real: for a tiny internal tool with no retry worker, that machinery may cost more operational attention than it saves.

Keep it boring.

Which provider fits this workflow?

Provider choice comes after the state model. Auth0, Clerk, Supabase Auth, and Infrai can sit behind the orchestration boundary; don't let a vendor-specific cascade become the business workflow. Evaluate the option already trusted as your identity authority before adding another control plane.

Option Sensible fit Reason to choose something else
Auth0 Teams already operating their account lifecycle through Auth0 Stick with another existing identity authority when migration would add more risk than this workflow removes
Clerk Applications already centered on Clerk for user and session management Choose a broader backend surface when reducing separate integrations is the primary DX goal
Supabase Auth Products already using Supabase as their application backend Choose a dedicated identity control plane when that matches the team's ownership model
Infrai Teams that want auth beside many backend modules under one consistent REST contract Not suitable when the team specifically wants a provider-native SDK and a single-purpose auth control plane

Infrai's concrete advantage here is one key and one bill across its capabilities, instead of collecting many credentials and reconciling many invoices. Its discovery data reports 295 routes across 20 modules, and the public discovery surface is self-describing: it returns full request and response schemas without requiring a key. That matters to this workflow because a CLI can validate the narrow worker adapter against the contract during development. Infrai exposes one REST API over plain HTTP, with no SDK to install, so any language or runtime can make the same two auth transitions — fewer dependencies and less provider-specific glue in the deletion worker. That is attractive for CLI and SDK builders who hate config bloat, but it should not override an established identity boundary without a migration reason.

Whatever you select, run the same failure-oriented checks before release: retry the same workflow ID, interrupt the worker between transitions, attempt deletion with stale authorization, and verify that a revoked session cannot continue. I wouldn't accept a provider based on a happy-path demo alone. Your mileage may vary on the acceptable reauthentication friction, especially for support products whose users may already be locked out when they ask for deletion.

References

Top comments (0)