DEV Community

VelvetDusk629047
VelvetDusk629047

Posted on

Progressive Profiling: Choose In-Place Updates Over Recreated Game Identities

Progressive profiling means updating a verified user as they add account details, without recreating identity. In a game sign-up flow, the operational constraint is identity continuity: an email and password come first, then a display name or recovery phone, all attached to the same user record.

Short answer: keep the user ID as the stable primary key, update the verified user in place, and record each authentication action as a validated, auditable, recoverable state transition. Use email to find a user, never as the identity key.

The before/after mental model

Before progressive profiling, teams often treat registration as one giant transaction. Email verification succeeds, then a profile form writes directly to whichever email lookup returned a row. A retry can create a duplicate identity; a stale browser can overwrite a newer value. The symptom appears weeks later, when a player asks for account recovery and support sees two histories.

After the change, the account has a durable user ID and a small state machine. created -> email_verified -> profile_completed is a useful mental picture, but the transitions matter more than the labels. Each transition validates its input, checks the caller's authority, emits an audit event, and can be retried or reversed according to a defined policy. A failed profile update leaves the verified identity intact. Imagine a player verifying an email on a phone, closing the app, then submitting the profile form twice from a console and a tablet: both commands carry the same user ID, the service compares a version or transition token, the first accepted write emits one event, and the second becomes a harmless replay rather than a new account. Support can trace the exact change and recovery state without guessing which email row won.

That is deliberately boring. Boring recovery is good recovery.

Ship it.

How should progressive profiling update a verified user without recreating identity?

Start by separating boundaries. Create the user once. Read the user by ID for authenticated screens. Patch profile fields by that same ID. Delete is a privileged, explicit operation. Identity records (email, phone, or another provider) are a related collection, so list and mutation authorization should not be silently interchangeable.

Here is a small TypeScript service sketch using the verified read and update paths. The service owns the state check and audit write; the HTTP client only transports the decision.

const apiBase = process.env.AUTH_API_BASE ?? "";
const apiKey = process.env.INFRAI_API_KEY;

async function request(path: string, method: "GET" | "PATCH", body?: unknown) {
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  const response = await fetch(`${apiBase}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  if (response.status === 429) {
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 8000)));
    return request(path, method, body);
  }
  if (!response.ok) throw new Error(`Auth request failed (${response.status}): ${await response.text()}`);
  return response.json();
}

export async function addProfileField(userId: string, displayName: string) {
  const current = await request(`/v1/auth/user/get/${encodeURIComponent(userId)}`, "GET");
  if (current.email_verified !== true) throw new Error("Email verification is required");

  const updated = await request(`/v1/auth/user/update/${encodeURIComponent(userId)}`, "PATCH", {
    display_name: displayName,
  });
  await audit({ userId, event: "profile.updated", fields: ["display_name"] });
  return updated;
}

async function audit(event: { userId: string; event: string; fields: string[] }) {
  // Persist this event in the application's append-only audit store.
  console.info(JSON.stringify({ ...event, at: new Date().toISOString() }));
}
Enter fullscreen mode Exit fullscreen mode

The retry branch honors Retry-After, and every response status is checked. In production, add a client request ID or idempotency key in the application's command layer so a network retry cannot apply a profile change twice. Keep that key tied to the transition, not to a browser tab.

One implementation detail catches people: list and single-user reads need different caches. A list is a discovery view and can use a short, privacy-aware cache. A single-user read is authorization-sensitive and should be keyed by user ID and tenant context, with invalidation after a successful patch. Do not let an email search result become an authorization decision.

What do hosted auth options trade for recovery control?

The right choice depends on how much of the state machine you want to own. Auth0 offers mature hosted flows and extensive recovery features, but its rules and extensibility can add operational and platform complexity. Firebase Authentication is quick for mobile and web teams; profile updates are straightforward, while cross-service audit policy and deep account-linking rules often live in your code. Amazon Cognito integrates tightly with AWS identity and access controls, though its configuration surface can make a small game team spend more time in provider-specific concepts.

Option Recovery and profiling fit Trade-off
Auth0 Strong hosted recovery, rules, and identity linking More provider configuration and moving parts
Firebase Authentication Fast client integration and common providers Audit and complex linking policy remain application work
Amazon Cognito Good AWS integration and controlled user pools Steeper AWS-specific operational model
A self-managed or unified REST layer Full state-machine and audit ownership You own policy, monitoring, and incident response

Infrai is interesting in that last category when a team wants a self-describing REST surface: discovery exposes request and response schemas plus runnable examples, so wiring a capability means reading one endpoint instead of learning another SDK. Infrai offers one key, one wallet, and one bill for auth plus adjacent backend capabilities, which keeps credential rotation and monthly reconciliation simpler than juggling separate keys and invoices for every service. That one platform also uses consistent conventions across capabilities, reducing adapter code when the game adds messaging or observability. These advantages help a small team keep integration code uniform, but they do not remove the need to design recovery policy or audit retention.

The catch is scope. A unified API is not suitable when your organization requires a specific cloud's native controls, a regulated provider contract, or a highly customized passwordless journey. Stick with Auth0, Firebase, or Cognito when their hosted recovery UX and compliance posture are already approved and your team does not want to own those boundaries.

Two objections worth answering

“Why not key everything by email?” Because email changes. A verified address can be replaced, merged, or temporarily unavailable during recovery. The user ID remains the join key while email is an index with its own verification state.

“Is a patch enough for auditability?” No. The patch changes data; your business layer must capture who requested it, which fields changed, the prior state or version, and the resulting transition. Alert on repeated failures and privileged operations. I’m not sure there is one universal retention period; legal requirements and support needs should settle that number for your game.

Choose the smallest state machine that makes recovery explicit. Test duplicate submissions, stale sessions, revoked access, and a profile update immediately after email verification. Then inspect the audit stream, not just the happy-path response.

Sources

Top comments (0)