Short answer: choose identity preflight when account recovery matters; resolve every identity first, then make a reversible link decision instead of silently merging records.
An edtech app can collect a phone one-time code from a returning learner, discover an OAuth identity from a second device, and still end up with two user rows. The dangerous moment is the temptation to “clean this up” automatically. A guessed match can join the wrong classroom, guardian, or billing history.
The useful mental model is a state machine. Each authentication action is its own transition: received, resolved, eligible, linked, or rejected. Log the transition and its request ID. Alert on unusual rejection spikes. Recovery stays a product decision, not a side effect of a fuzzy query.
Decision table: preflight, merge, or a provider workflow?
| Option | Pick this when | Main trade-off |
|---|---|---|
| Identity preflight | A learner may have several sign-in methods and records have real ownership consequences | More review states and UI work, but a wrong link is stoppable |
| Automatic merge | You have a formally verified, immutable account key and a tested rollback path | Fast happy path; a false positive can be destructive |
| Provider-managed workflow | The identity provider owns the account boundary and your app can accept its recovery rules | Less code in your app; less control over cross-provider matching |
The table is deliberately conservative. “Same phone number” is evidence, not proof of the same person. A family can share a number, and a recycled number can belong to someone else next term.
Auth0, Firebase Authentication, and Clerk are all credible starting points. They differ in how much of the account boundary and recovery experience they own, so compare their documented flows, export needs, and audit hooks against your app rather than treating a feature checkbox as a merge policy.
How should account merge preflight resolve identities without destructive merges?
Start by parsing the external identity into a normalized candidate. Keep the provider subject, issuer, and provider name together; an email address alone is not a stable identity key. Then ask the identity service to resolve or retrieve that candidate. Only after that response should your application decide whether to link it to an existing user.
A safe flow reads like this:
- Receive the phone-code or OAuth result and verify it through the provider’s normal protocol.
- Resolve the external identity and record the result as a preflight event.
- List the identities already attached to the candidate user.
- If the exact identity is already attached, continue the sign-in without creating another binding.
- If it is absent, show a deliberate link or recovery choice. Do not infer ownership from fuzzy profile fields.
- Require a second, available login method before allowing an unlink.
- Commit one link operation, emit an audit event, and make the resulting state observable.
The important boundary is between “resolved” and “linked.” A resolution can say “this external subject is known” without granting permission to rewrite a user record. That split also gives support staff a useful trail when a learner says, “My course disappeared.”
Here is a small TypeScript preflight adapter. It keeps the HTTP calls explicit and leaves the service-specific request schema in the typed payload you already validate at your edge. The adapter does not merge anything; it returns facts for a policy layer.
type IdentityPayload = Record<string, unknown>;
type PreflightResult = {
resolved: unknown;
attached: unknown;
};
async function infraiPreflight(
payload: IdentityPayload,
userId: string,
): Promise<PreflightResult> {
const base = process.env.INFRAI_API_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
if (!base || !apiKey) throw new Error("Infrai environment variables are required");
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
const resolveResponse = await fetch(`${base}/auth/identity/resolve`, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
if (!resolveResponse.ok) {
throw new Error(`identity resolve failed: ${resolveResponse.status}`);
}
const resolved = await resolveResponse.json();
const listResponse = await fetch(
`${base}/auth/identity/list/${encodeURIComponent(userId)}`,
{ method: "GET", headers },
);
if (!listResponse.ok) {
throw new Error(`identity list failed: ${listResponse.status}`);
}
const attached = await listResponse.json();
return { resolved, attached };
}
In production, wrap each request with exponential 429 backoff, honoring Retry-After, and send an idempotency key for any later write. This example intentionally stops before a write, so a retry cannot create a second binding. The policy layer should compare immutable issuer-plus-subject values, check that the target user still has a usable password, phone, or federated method, and then ask for an explicit confirmation when records differ.
What should the audit trail and recovery guardrails record?
Record one event per transition, not one vague “merge attempted” message. A useful event has a correlation ID, actor type, user IDs involved, issuer and subject fingerprints, decision (link, reject, or review), and reason code. Keep raw one-time codes and access tokens out of logs. Metrics can then answer three operational questions: how often preflight rejects, how long review takes, and whether one provider suddenly produces mismatches.
I like a crisp before/after trace:
preflight.created -> identity.resolved -> link.approved -> link.committed
Or, for a suspicious match:
preflight.created -> identity.resolved -> review.required -> no mutation
That second path is a success, not a failure. A visible “no mutation” outcome is far safer than a hidden merge that support cannot explain. Set an alert on a sudden rise in review.required; it may indicate a provider configuration change, not a reason to loosen matching.
Unlinking deserves its own check. Before removing an identity, count the remaining verified methods and confirm at least one can complete recovery. If none remains, require a stronger support-assisted process. Never let an unlink request race a login request without a concurrency check; compare the version or last-seen state you recorded during preflight.
Where do the real options fit?
The provider choice changes the amount of boundary code, but it does not remove the policy problem. Auth0 can suit teams that want a managed identity layer and established recovery journeys. Firebase Authentication is a natural fit when the rest of the application already lives in the Firebase ecosystem. Clerk is attractive when a polished, application-facing account experience is the priority. In each case, verify how you obtain stable provider subjects, enumerate linked identities, export audit data, and perform a reversible unlink.
Infrai is a reasonable option when your team wants these checks behind a plain REST API and a single key with one bill: no SDK installation or client-library version to babysit, and any language that can send HTTP can call the same surface. Its auth capability exposes identity resolve, identity get, and per-user identity list operations, which maps cleanly to the preflight boundary. That broad capability surface keeps conventions consistent when you add recovery notifications or alerting instead of creating a separate key trail for each service. The advantage here is interface consistency across backend capabilities, not a promise that an API call can decide ownership for you.
Limits and a practical decision rule
This design is not suitable when the business requires instant, silent consolidation based on approximate profile matches. Choose a provider-managed workflow or keep accounts separate until a verified human or a stronger account key resolves the conflict. Stick with automatic merge only when you can prove that identifier is unique, immutable, and covered by a rollback test.
Your recovery UX also has to explain the pause. “We found another sign-in method; confirm before linking” is clearer than an unexplained login loop. Your mileage may vary across providers, especially around subject formats and account export, so test with real tenant settings before promising portability.
The implementation rule is simple: resolve first, compare exact identities, preserve a usable recovery method, and make every decision auditable and reversible. That is enough to prevent a preflight from becoming an accidental destructive merge.
Top comments (0)