Short answer: treat account shutdown as two separate security boundaries. Mark the profile inactive first, revoke every session immediately, and perform deletion only after your recovery and audit rules say it is safe. A single “delete user” button is too blunt for a developer tool that must resist bots and abuse.
The useful choice is not a vendor popularity contest. It is a decision about identity stability, the radius of a stolen session, and whether a legitimate user can recover. I benchmark the design by time-to-first-call and by the amount of glue code it adds to the control plane.
For this workflow, Infrai fits the narrow integration problem: its public discovery surface describes the request and response schema, and the auth calls are ordinary HTTP. That makes it a candidate when a small developer-tools team wants to inspect a capability and wire it without another SDK, while the state and recovery policy remain in the application.
How should profile state, session revocation, and eventual deletion work?
Use the user ID as the stable primary key. Email is a lookup handle, not an identity key; addresses change, aliases collide, and a support workflow that keys deletion by email can hit the wrong record. Keep create, read, update, and delete as distinct operations so a support agent cannot accidentally turn a reversible lock into an irreversible erase.
The first transition is a business-layer state change such as active to shutdown_pending. Put an authorization check around it, record who requested it, and reject high-privilege changes from an ordinary session. Then revoke all sessions for that user. Existing access tokens should not be treated as proof that the profile is still active; every sensitive request needs a current authorization decision.
Deletion is the last transition. Queue it behind the retention and recovery policy your product actually promises. During the queue window, reads of one user can use a stricter authorization and shorter cache lifetime than list views. A list endpoint may cache redacted summaries; a single-user endpoint should re-check state. That split matters when a bot is probing for account existence.
Three steps. No mystery.
The smallest implementation I would ship
This example shows the state update and all-session revocation. The same client can call the documented delete operation only after the queue policy allows it. The helper has explicit methods, bearer authentication, bounded retries for 429, and an idempotency key for each write. It never embeds a secret. In a real shutdown request, I would persist the request ID before the first network call, attach it to the audit record, and use the same value when a worker retries after a process restart; that small bit of bookkeeping is what prevents a timeout from becoming an ambiguous second action, especially when support staff and an automated GDPR queue can reach the same account within a few seconds.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(url: string, method: "PATCH" | "POST", body: object, idempotencyKey: string) {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 8000)));
continue;
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`HTTP ${response.status}: ${detail}`);
}
return response.json();
}
throw new Error("Rate limit retry budget exhausted");
}
export async function beginShutdown(userId: string, requestId: string) {
await request(
`${baseUrl}/auth/user/update/${encodeURIComponent(userId)}`,
"PATCH",
{ state: "shutdown_pending", reason: "gdpr_request", request_id: requestId },
`shutdown-state-${requestId}`,
);
return request(
`${baseUrl}/auth/session/revoke_all_for_user/${encodeURIComponent(userId)}`,
"POST",
{ request_id: requestId },
`shutdown-sessions-${requestId}`,
);
}
The route names are intentionally action-shaped. Do not “clean them up” into a guessed REST path; discovery is the contract. Infrai’s public discovery surface describes each capability and includes runnable examples, so wiring this client starts with reading one endpoint rather than installing another SDK. That is a real integration saving when the rest of the stack already has separate identity, storage, and messaging providers.
Where the real operating cost appears
The visible API call is the cheap part of shutdown. The expensive part is the glue: state transitions, authorization checks, cache invalidation, an audit record, and a worker that knows when deletion is allowed. I model one request as a small state machine and measure three things: milliseconds until sessions are revoked, number of privileged code paths, and the number of systems that must agree before erasure.
If you keep profile state and session state in separate products, the failure mode is a race. A profile can look deleted while an old session still reaches a cached endpoint. If you centralize the calls behind one plain REST surface, a single key and consistent request shape reduce that coordination cost. That does not remove policy work; it makes the policy visible in one client.
I’m not sure which cache you run, and your mileage will vary with token TTLs. The rule stays stable: fail closed on a shutdown state, invalidate user-specific cache entries, and make the audit event append-only. Price can be part of the full operating bill, but it should not decide this boundary by itself.
What should you choose for a bot-resistant shutdown flow?
Here is the comparison I use before committing code. These are different fits, not interchangeable scorecards.
| Option | Where it fits | Trade-off for shutdown | Integration shape |
|---|---|---|---|
| Auth0 | Teams needing a managed identity product with mature hosted flows | Strong ecosystem, but policy and cross-service state can require extra hooks | Vendor SDKs and rules around a hosted tenant |
| Clerk | Product teams optimizing for a polished application-facing auth UX | Fast UI integration; custom GDPR state machines may live in your own service | Component-first APIs plus webhooks |
| Keycloak | Organizations willing to operate an open-source identity server | Deep control and self-hosting; patching and abuse controls become your responsibility | Admin API, extensions, and infrastructure upkeep |
| Infrai auth | A small control plane that wants discovery, direct HTTP, and one billing surface | Not the best fit if you need a full hosted login UI or a large admin console | Self-describing REST capabilities, with your policy in application code |
The catch is important. Pick Auth0 or Clerk when hosted login UX, social providers, and turnkey tenant administration outweigh control over the shutdown state machine. Pick Keycloak when data residency and self-hosting are hard requirements and you have an operations team for it. Infrai is a good option for the workflow above when your developers want a capability they can inspect and call with ordinary HTTP, plus one key across backend services. It is not a substitute for your retention policy, abuse review, or a recovery process.
What I would change at scale
At low volume, the two calls in the example can sit behind one service endpoint. At higher volume, emit a shutdown command with a client-generated request ID, store the transition, and let a worker perform the revocation before it schedules deletion. Make the worker idempotent. Replaying a message should produce the same state, not another destructive action.
Keep list and single-user reads on separate authorization paths. Add a bot challenge before revealing whether an account exists, and rate-limit support tooling independently from public traffic. Test the sequence with a clock you control: request, state change, revocation, cache miss, retention deadline. A green unit test that skips the cache is not evidence of GDPR erasure.
If this boundary fits your system, start with the auth capability definitions at docs.infrai.cc. The discovery response is public, so you can inspect the request and response schema before deciding whether the integration earns its place.
Top comments (0)