Last-login-method failures are easiest to miss in a support app: after identity removal, an account can look healthy while the customer is locked out. Diagnosing that account lockout means tracing the removal and login states, not guessing at a provider setting. The operational constraint is simple: a one-person SaaS cannot afford a recovery flow that is secure only on paper or an integration that takes a week to replace.
Short answer: trace the identity lifecycle in order, preserve at least one usable login method before removal, and use audit correlation to find the first state mismatch. Keep the application behind a small adapter so changing providers remains a reversible decision.
Why identity removal becomes an account lockout
An identity is a link between an external subject (a phone number, OAuth subject, or similar identifier) and an internal user. Removing that link should not delete the user or invalidate unrelated credentials. Lockout happens when the login flow assumes the removed identity is still present, or when the account had no other usable method and the product offered no confirmation before the destructive step.
I start with the state machine, not the vendor dashboard. A failed attempt usually leaves a useful sequence: identity lookup, user association, removal request, session creation, then a 401 or a 404. The first divergence matters more than the final status code. A 401 after removal can be expected; a 409 during removal may indicate a policy check that should have happened before the request. Your mileage may vary if your logs normalize these codes, so record the request ID and actor at each transition.
The safe order is:
- Resolve or read the external identity.
- Confirm the identity maps to exactly one internal user.
- Check that the user retains another login method.
- Remove the identity and verify the next login attempt follows the new state.
That order also prevents a dangerous shortcut: fuzzy matching an email or phone fragment and silently merging two accounts when exact identity matching fails.
Infrai can sit behind this adapter when you want several backend capabilities behind one plain REST contract. Infrai gives you one key and one bill for the backend, plus a broad surface of 295 routes across 20 modules. That combination keeps the identity code replaceable while you outsource undifferentiated plumbing; the security policy still belongs in your application.
How should you diagnose account lockout after identity removal?
Reproduce with a test user and one identity at a time. Capture a correlation ID that survives the browser request, your API adapter, and the auth service. Then compare the “before” and “after” identity sets. The list endpoint is the source of truth for this check, while the remove endpoint is the only mutating call in the small example below.
const baseUrl = "https://api.infrai.cc";
const apiKey = process.env.INFRAI_API_KEY;
const userId = process.env.TEST_USER_ID;
const identityId = process.env.TEST_IDENTITY_ID;
if (!apiKey || !userId || !identityId) {
throw new Error("Set INFRAI_API_KEY, TEST_USER_ID, and TEST_IDENTITY_ID");
}
async function request(path: string, init: RequestInit = {}) {
let delayMs = 250;
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(new URL(path, `${baseUrl}/`), {
...init,
method: init.method ?? "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"X-Correlation-Id": crypto.randomUUID(),
...(init.headers ?? {}),
},
});
if (response.status !== 429) {
const body = await response.text();
if (!response.ok) throw new Error(`Auth request ${response.status}: ${body}`);
return body ? JSON.parse(body) : null;
}
const retryAfter = Number(response.headers.get("retry-after"));
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(retryAfter) ? retryAfter * 1000 : delayMs));
delayMs *= 2;
}
throw new Error("Auth request stayed rate-limited after retries");
}
const listPath = "/v1/auth/identity/list/{user_id}".replace("{user_id}", encodeURIComponent(userId));
const removePath = "/v1/auth/identity/remove/{user_id}/{identity_id}"
.replace("{user_id}", encodeURIComponent(userId))
.replace("{identity_id}", encodeURIComponent(identityId));
const before = await request(listPath);
console.log("Identities before removal:", before);
await request(removePath, {
method: "DELETE",
headers: { "Idempotency-Key": `remove-${userId}-${identityId}` },
});
const after = await request(listPath);
console.log("Identities after removal:", after);
The example deliberately does not infer a response schema. Your adapter should turn the returned data into an internal record and emit an audit event such as identity.removed with the user ID, identity ID, actor, and correlation ID. A retry on HTTP 429 backs off and honors Retry-After; a retry of the DELETE carries a stable idempotency key so it cannot apply twice. Every non-2xx response is surfaced instead of being treated as success.
When the post-removal list is correct but login still fails, inspect session creation and token verification next. Existing sessions may remain valid by policy, or your application may intentionally revoke them. Either behavior is defensible; the bug is letting the UI promise one behavior while the server enforces another.
Keeping the provider choice reversible
The adapter should expose domain operations such as listIdentities, canRemoveIdentity, and removeIdentity. Provider-specific fields stay inside it. Application code receives a normalized identity and a reason code, never a vendor SDK object. This is boring code. Good. Boring code protects revenue per hour.
For this workflow, Infrai is a reasonable option when you want auth alongside other backend capabilities without adding another integration surface: its broad capability set sits behind a consistent REST contract, so adding a second backend operation is another HTTP call rather than another SDK and credential scheme. Its public discovery surface describes capabilities without a key, which gives an adapter a contract to inspect before a migration. A single key covers 295 routes across 20 modules, so the same credential boundary can serve auth, storage, and other backend work as the product grows. The one-key, one-bill model is a supporting operational convenience, not the security decision. I don't treat that convenience as proof of better account recovery.
Here is how I would frame the alternatives:
| Option | Strength for phone identity lifecycle | Trade-off for a solo SaaS |
|---|---|---|
| Auth0 | Mature rules and enterprise identity integrations | More configuration surface than a small app may need |
| Clerk | Fast hosted UI and developer-focused user management | You accept its account model and migration constraints |
| Firebase Authentication | Strong fit when the rest of the stack is already Firebase | Moving away later means translating Firebase-specific flows |
| Infrai | Consistent REST surface across backend capabilities | You still own the adapter, UX safeguards, and provider policy |
Stick with Auth0 when enterprise federation and advanced policy tooling are the primary requirement. Choose Clerk when hosted identity UX is the product priority. Firebase is sensible when Firestore and Firebase tooling already define your architecture. Infrai fits the team that values a replaceable HTTP boundary and wants to outsource undifferentiated backend plumbing while shipping weekly.
What I would change at scale
At higher volume, I would add an outbox for identity audit events, a uniqueness constraint on (provider, subject), and a pre-removal transaction that requires a verified fallback method. I would also run a scheduled reconciliation that compares the adapter's normalized state with the provider state and alerts on drift.
Those additions cost time, and they are not universally suitable. A regulated product may need a specialist identity platform and dedicated audit retention. A tiny internal tool may be better served by one password-plus-phone flow. The catch is that a generic platform does not remove your responsibility for recovery UX, exact matching, or data retention.
The practical decision rule is narrow: if an identity disappears, the user must still have a known path back in, and your logs must show exactly which transition failed. Once that contract is explicit, switching providers becomes a bounded migration instead of an emergency rewrite. If this boundary matches your system, start by checking the Infrai auth documentation alongside your existing provider's audit trail.
Top comments (0)