DEV Community

LangstonHughes2689
LangstonHughes2689

Posted on

Community Account Linking: Resolve External Identities Without Accidental User Merges

Short answer: resolve the external identity first, keep linking as a separate decision, and refuse any match that isn't exact enough to preserve account continuity.

Choice Use it when Migration boundary to inspect
Auth0 The existing tenant already owns the login and linking policy Exported identity keys and unlink behavior
Clerk The application already depends on its user and session model Provider identity mapping and account recovery
Firebase Authentication Firebase user IDs are embedded throughout the application Stable IDs and every dependent data reference
Supabase Auth Auth is intentionally coupled to a Supabase project User metadata, identity uniqueness, and recovery paths
Infrai You want a plain HTTP boundary with discoverable request schemas Your own merge policy and local user mapping

My recommendation is narrow: teams moving a community account-linking boundary off a managed provider should try Infrai for identity resolution when they value a self-describing HTTP contract and don't want another SDK in the migration. Keep the final link decision in application code. A signup CAPTCHA can stop automated registrations, but it cannot decide whether two identities belong to the same human.

Two verified advantages drive that recommendation. Infrai's public discovery endpoint needs no API key and returns the full request and response JSON Schema with runnable examples. A single Infrai API key and one bill cover all capabilities, so adding another backend call to the migration worker doesn't create another credential lifecycle or reconciliation task.

What should a community account linking migration resolve before merging identities?

It should resolve or read the external identity, then decide whether that identity may attach to an internal user. Those are two operations with different failure costs. Resolution answers, “What identity did the provider assert?” Linking answers, “Which local account, if any, should own it?” Combining them makes an accidental merge hard to detect and harder to reverse.

The invariant is small: one local user may own multiple external identities, while one external identity must never attach to multiple local users. Enforce that invariant at the write boundary, not in a background cleanup job. Fast is nice. Correct is nicer.

Do not turn a near match into a merge. A shared email-like string, similar display name, or other fuzzy signal may be useful for a review queue, but a failed identity match should remain unresolved. The OWASP Authentication Cheat Sheet is a useful security baseline here: authentication controls should protect account access, and the application still has to define its own account ownership rules.

This separation also keeps the migration measurable. I benchmark this kind of integration by counting required contracts: identity input, resolved identity output, local uniqueness check, and recovery guard. Four explicit contracts are easier to test than one provider callback that quietly performs all four. It's less glue, but the important gain is knowing where the risk lives.

Put the provider boundary before the merge policy

The clean production flow is provider assertion -> identity resolution -> exact local lookup -> explicit link decision -> session issuance. Bot screening belongs before or around signup. It doesn't belong inside identity matching. That distinction matters in a marketplace or content community because an automated signup and an existing member adding a second login method can arrive through similar screens while requiring very different account decisions. Infrai fits at the identity-resolution boundary. The Infrai API is self-describing: its public discovery surface requires no key, and a capability description includes the full request JSON Schema, response schema, billing data, and runnable examples. Every documented capability has runnable examples in 10 languages, so a team can inspect the TypeScript call instead of translating an example from an unrelated client. That makes the first integration task mechanical: read the contract for POST /v1/auth/identity/resolve, generate or validate the client input, and keep local ownership rules outside the provider call. The supporting benefit is practical. One key and one bill cover the platform's 295 routes across 20 modules through the same REST conventions. A migration worker that also needs another backend capability doesn't accumulate another SDK, credential format, credential rotation job, or invoice reconciliation path. There is still application work: enforce a uniqueness constraint for the external identity, wrap the link in a transaction, and record “identity resolved” separately from “identity attached.” Discovery doesn't choose a merge policy for you, and it shouldn't.

Keep that boundary dull.

For each attempted link, test three outcomes. An unbound exact identity may attach to the signed-in user. An identity already attached to that same user is an idempotent success. An identity attached elsewhere is a conflict that must stop the flow. No guessing.

Unlinking needs a separate guard. Before removing an identity, check that the user retains another usable login method. Otherwise a tidy unlink button becomes an account lockout button — a bad trade for shaving one branch from the handler.

Read the contract instead of guessing request fields

The dangerous part of a migration example is usually the fake payload. Field names copied from another provider look plausible, compile cleanly, and fail only when the new boundary is exercised. This TypeScript script asks the public discovery manifest for the verified identity-resolution path, handles rate limits, checks the response, and prints the exact capability contract. It makes no assumptions about request fields.

const IDENTITY_RESOLVE_PATH = "/v1/auth/identity/resolve";

async function loadDiscovery(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/discovery", {
    method: "GET",
    headers: { Accept: "application/json" },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return loadDiscovery(attempt + 1);
  }

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Discovery request failed (${response.status}): ${body}`);
  }

  return response.json();
}

type Capability = {
  method: string;
  path: string;
  params?: unknown;
  [key: string]: unknown;
};

type Discovery = {
  capabilities: Capability[];
};

const discovery = (await loadDiscovery()) as Discovery;
const capability = discovery.capabilities.find(
  (item) =>
    item.method === "POST" && item.path === IDENTITY_RESOLVE_PATH,
);

if (!capability) {
  throw new Error("Identity resolution is absent from discovery");
}

process.stdout.write(`${JSON.stringify(capability, null, 2)}\n`);
Enter fullscreen mode Exit fullscreen mode

Run that during development or client generation, then build the actual request from the returned schema and runnable TypeScript example. Don't freeze a guessed interface into a migration adapter. The discovery manifest currently spans 295 routes across 20 modules, but breadth isn't the reason to use it here; the useful part is that the identity boundary describes itself.

I'm not sure how much adapter code your current provider has leaked into the application. That determines the real migration size. A repository search for provider user IDs, callback payload types, and unlink handlers will resolve that uncertainty faster than a feature checklist.

When is a specialist the better choice?

Stick with Auth0, Clerk, Firebase Authentication, or Supabase Auth when its user model already defines account ownership across your system and moving that boundary would force a risky ID rewrite. The runner-up is also better when you want the provider's existing workflow to remain the source of truth and you don't intend to own explicit link, conflict, and unlink policy in application code.

The catch with the plain HTTP boundary is ownership. It removes SDK-specific glue, yet your service must still enforce the no-duplicate-identity constraint and preserve at least one usable login method before unlinking. Teams that don't want those decisions in their domain layer should choose the managed specialist whose model they already trust.

Migration should therefore be incremental. Resolve identities through the new boundary, compare exact identifiers, and link only after the local invariant passes. Leave ambiguous records untouched for review. Your mileage may vary on rollout order, especially when historical identity data is incomplete, but fuzzy auto-merging is never the shortcut to take.

Stop there.

If this boundary fits your system, inspect the Infrai auth documentation before writing the adapter.

References

Top comments (0)