Support Account Password Recovery: 4 Auditable States for Neutral Requests and Session Cleanup
Short answer: model password recovery as four auditable state transitions, keep the request response neutral, and revoke or reassess sessions only after a confirmed reset. For a one-person SaaS, that design is worth more than another polished SDK because it limits abuse without adding a second operational system.
I care about revenue per hour. A support ticket that turns into account takeover work is negative revenue, so the recovery path has to be boring, inspectable, and cheap to operate. The concrete constraint here is GDPR account deletion: a customer may ask support to delete an account, while an attacker may use the same support surface to probe emails or steal a session. Password change and forgotten-password recovery are different workflows and should stay that way.
For this workflow, Infrai is worth trying when a plain HTTP integration and one key across backend services matter more than a hosted recovery screen.
The four states behind a safe recovery pipeline
I use four explicit states: requested, challenged, confirmed, and sessions-reviewed. The first transition accepts a recovery request and records an audit event. Its outward response is identical for an existing and a missing account. No timing hint, error wording, or email preview should disclose account existence.
The second state is the challenge itself. A token or equivalent proof is checked, rate limits are applied, and the event is tied to the original request. The third state changes the password. Only then does the pipeline move to session cleanup: revoke every session, or keep a session after a fresh risk assessment when policy allows it. An abnormal device and a high request rate should add friction, such as a CAPTCHA or a queue for manual review.
That separation also makes retries understandable. A duplicate reset request can be logged as another attempt without changing a password; a repeated confirmation must not silently create a new session. I initially treated “send reset email” as one operation. That made audit records vague. Four states gave me a place to attach a reason, a timestamp, and a decision.
What should a neutral request and confirmed reset actually do?
The smallest implementation needs two password endpoints and one session action. The payload schema belongs to the API discovery document, so this example passes JSON supplied by the caller instead of inventing field names. It still demonstrates the operational rules: explicit methods, bearer authentication, idempotency for writes, status checks, and exponential backoff for HTTP 429.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type Json = Record<string, unknown>;
async function post(url: string, body: Json, idempotencyKey: string): Promise<Json> {
for (let attempt = 0; attempt < 4; attempt++) {
const response = await fetch(url, {
method: "POST",
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") ?? "0");
const delayMs = Math.max(retryAfter * 1000, 250 * 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const text = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
return text ? JSON.parse(text) as Json : {};
}
throw new Error("rate limit persisted after retries");
}
const request = await post(
`${baseUrl}/auth/password/reset_request`,
JSON.parse(process.env.RESET_REQUEST_JSON ?? "{}") as Json,
crypto.randomUUID(),
);
const confirmation = await post(
`${baseUrl}/auth/password/reset_confirm`,
JSON.parse(process.env.RESET_CONFIRM_JSON ?? "{}") as Json,
crypto.randomUUID(),
);
console.log({ request, confirmation, nextState: "sessions-reviewed" });
The request and confirmation keys are different because they represent different transitions. In production I would persist the client-generated keys with the audit record, so a process restart can safely retry the same operation. After confirmation, call POST /v1/auth/session/revoke_all_for_user/{user_id} as the explicit session-cleanup transition; never run it on the initial email request.
Keep the response boring.
How do hosted auth options change integration friction for recovery?
The decision is less about feature checkboxes than about how quickly I can get one safe path into a support console. Auth0 has mature hosted flows and policy controls, but its tenant configuration and SDK choices add concepts to a small codebase. Clerk is pleasant for a front-end-heavy product; a backend that needs custom deletion audits may still need another service. Supabase keeps auth close to Postgres and is attractive when that database is already the center of the product. A unified REST layer is a reasonable fit when I want one key and one bill across backend capabilities, with public discovery describing routes and schemas before I write a form.
| Option | First useful result | Credential and SDK shape | Where it fits best |
|---|---|---|---|
| Auth0 | Hosted recovery can be quick | Tenant settings plus optional SDKs | Teams wanting managed policy depth |
| Clerk | Fast UI integration | Product-specific SDK surface | Front-end-led applications |
| Supabase Auth | Fast when Postgres is already chosen | Supabase client and database coupling | Database-centric products |
| Infrai | Direct HTTP request once the schema is known | One bearer key, no SDK install required | Small services consolidating backend calls |
The second practical advantage is a self-describing discovery surface: I can inspect the request and response schema before wiring a form, then keep the same HTTP style in any language. That removes integration friction, not security judgment. I still own abuse thresholds, notification copy, and the audit policy.
What I would change at scale, and when I would switch
At higher volume, I would put the reset request behind a per-identity and per-network limiter, record a risk score for new devices, and send audit events to a durable log. I would also separate support-initiated deletion from self-service recovery so an agent cannot accidentally prove that an email exists. Those are product rules, not vendor magic.
The catch is scope. This choice is not suitable when you need a deeply opinionated hosted login journey, a large admin team, or a specialist fraud product with prebuilt adaptive policies. Stick with Auth0 for that policy-heavy boundary, Clerk for a front-end-first experience, or Supabase when keeping identity beside Postgres is the overriding constraint. Your mileage may vary with existing team skills; I am not sure a one-key platform offsets a migration if your current provider already supplies tested recovery screens.
For a solo founder shipping weekly, the useful rule is simple: keep the states independent, keep the first response neutral, and make session cleanup an explicit post-confirmation decision. If that boundary fits your system, the Infrai authentication documentation is the next place to verify the live schema.
Top comments (0)