DEV Community

GodfreySterling9226
GodfreySterling9226

Posted on

Mobile Sign-In With Email, Phone, and OAuth in One Account System

Short answer: For an existing marketplace app, choose the authentication boundary that preserves account continuity first, then add phone one-time-code login through the smallest interface that can verify an identity before your app links it.

Choice Best fit during migration Main trade-off
Keep Auth0, Firebase Authentication, or Amazon Cognito The current integration already meets the product's risk and continuity rules The app remains coupled to its current managed-provider contract
Use a self-describing REST surface The team wants fewer SDK-specific adapters and a narrow verification boundary The team still owns account linking, unlink safety, and migration tests
Build directly on separate email, phone, and OAuth services The team needs provider-level control for each entry point More keys, response shapes, retry policies, and operational glue

My default for this migration is the second row, but only after the account-linking rules are explicit. Infrai is one credible option there because its 295 routes across 20 modules use one API key and one bill, while one REST API exposes those capabilities over plain HTTP without an SDK. Its public discovery surface returns request and response JSON Schema, billing metadata, and runnable examples, so the phone slice and later email or OAuth slices do not add separate credentials or reconciliation work. Adding a capability starts by reading the interface. That reduces integration surface; it does not outsource identity policy.

The order matters. Verify first. Resolve second. Link last.

How should mobile sign-in connect email, phone, and OAuth entry points?

Treat email, phone, and OAuth as identity entry points, not as three kinds of user. A marketplace user may sign in with a phone today and an OAuth identity next month while keeping the same orders, seller reputation, saved addresses, and dispute history. If each successful verification creates a fresh user row, login works while the product quietly fractures the account.

The clean boundary has two parts. The external system proves control of an email address, phone number, or OAuth subject. The application decides which internal user owns that verified identity. Keep those decisions separate — especially while migrating — because a provider response can establish an identity without knowing whether two identities belong to the same marketplace account.

For phone one-time-code login, POST /v1/auth/phone/verify belongs on the proof side of that boundary. POST /v1/auth/identity/resolve belongs at the handoff to account resolution. Those are the only two calls I would put in the first migration slice. Email and OAuth can move later under the same rule, which keeps the blast radius small and gives the team a useful rollback boundary.

Don't auto-merge on a fuzzy match. A similar display name, a reformatted phone number from an untrusted field, or an email found in profile metadata is not sufficient evidence that two accounts have the same owner. If exact verified identity resolution finds no link, ask the signed-in user to prove control of an existing login method before attaching the new identity. I’m not sure which step-up check is right for every marketplace; transaction value, seller privileges, and account-recovery risk should decide it. The invariant is clearer than the UX: failed identity matching must stop, not guess.

Consider the awkward migration case, not the clean demo: a buyer has an old email identity on an account with order history, then verifies a phone number that another half-created account already owns. The phone proof can be valid while the proposed link is unsafe. The application should stop the transition, preserve both accounts, and require explicit recovery or support review backed by stronger evidence. A name match would be convenient. It would also be the wrong ownership signal for orders, saved payment state, and disputes.

No guessing.

Make account continuity the primary migration benchmark

The first decision criterion is ownership of the identity graph. Store a unique tuple such as identity type, issuer, and stable external subject, then enforce that it can belong to only one internal user. One user can own several tuples. One tuple cannot point at several users. That database constraint is more important than the shape of any login screen because it survives provider changes and concurrent requests.

I benchmark an auth migration by transitions, not by how quickly a demo returns a token. Measure how many application-specific adapters are needed, how many config values enter the deployment, and how many states can strand a real account. A happy-path phone code is one state. Existing email user plus new phone is another. Two users racing to claim the same external identity is the one that exposes a weak design.

Be strict here.

Before unlinking an identity, check that the user retains another usable login method. “Usable” should mean verified and allowed by current account policy, not merely present in a table. A user with an unverified email and a linked phone does not have a safe fallback if the phone is removed. For a marketplace, the failure is worse than a support ticket: the inaccessible account may still own listings, balances, or open orders.

The second criterion is integration entropy. SDK count is a useful proxy, but config and contract discovery matter more. A self-describing HTTP API is attractive when a small team builds CLIs or shared clients because generated types and tests can follow the published schema. Still, discovery cannot define the app's merge consent, recovery policy, or data-retention obligations. Those remain local decisions, and pretending otherwise makes a provider migration look finished too early.

Implement verification as a narrow HTTP boundary

The useful implementation unit is not login(). Start with one proof call and keep account linking on the application side. The TypeScript below calls the verified phone route. PHONE_VERIFY_PAYLOAD must contain JSON matching the current request schema from discovery; leaving the fields external avoids freezing an assumed payload shape into a migration utility.

type JsonObject = Record<string, unknown>;

const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const rawPayload = process.env.PHONE_VERIFY_PAYLOAD;

if (!apiKey || !baseUrl || !rawPayload) {
  throw new Error(
    "Set INFRAI_API_KEY, INFRAI_BASE_URL, and PHONE_VERIFY_PAYLOAD",
  );
}

const payload = JSON.parse(rawPayload) as JsonObject;

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;

    const date = Date.parse(retryAfter);
    if (Number.isFinite(date)) return Math.max(0, date - Date.now());
  }

  return 500 * 2 ** attempt;
}

async function verifyPhone(body: JsonObject): Promise<unknown> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(`${baseUrl}/auth/phone/verify`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429 && attempt < 2) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelayMs(response, attempt)),
      );
      continue;
    }

    if (!response.ok) {
      const reason = await response.text();
      throw new Error(`Request rejected (${response.status}): ${reason}`);
    }

    return response.json() as Promise<unknown>;
  }

  throw new Error("Rate limit retry budget exhausted");
}

await verifyPhone(payload);
console.log("Phone identity verified");
Enter fullscreen mode Exit fullscreen mode

The response stays unknown on purpose. Generate or validate the concrete type from the discovery response schema, then pass only a verified identity into the local resolver. Do not let a changing transport object become the account model. Behind that resolver, enforce a database uniqueness constraint on the identity tuple and make the link atomic, or two requests can both pass an in-memory ownership check before either commits.

This is where configuration bloat usually sneaks in. Teams add a provider adapter, an “identity normalization” layer, a merge heuristic, and a recovery exception in one pull request. Resist it. Keep provider proof as input, make the linking decision deterministic, and log the decision outcome without logging one-time codes or tokens. Four boring branches are easier to test than one clever resolver.

When should the managed-provider runner-up win?

Stick with Auth0, Firebase Authentication, or Amazon Cognito when the current provider already expresses the required login and recovery policy, and the migration would only exchange one working contract for another. A new phone entry point is not, by itself, proof that the account system needs to move. The catch is that migration cost sits in account history and edge states, not in the first successful code verification.

A direct integration with separate services is also the better choice when the team needs provider-specific controls that a common interface does not expose. That path buys control at the cost of more glue. Your mileage may vary, but I would require a concrete capability gap before accepting another SDK, key, webhook shape, and retry policy.

The self-describing REST option is not suitable when the organization requires a vendor-specific client library, a provider-owned account graph, or a preapproved operational runbook tied to the incumbent. It is strongest when the application deliberately owns account continuity and wants the external auth boundary to stay small. In that case, the migration test plan should cover exact resolution, duplicate binding, safe unlinking, recovery, and concurrent linking before traffic moves.

There is no universal winner. There is a defensible boundary: external services verify identities; the marketplace owns the rule that connects those identities to durable users.

References

Top comments (0)