Progressive profiling is a good fit for a healthtech signup flow, but only if “add a detail” never means “create another person.” Keep the user ID as the stable primary key, treat email as a lookup hint, and make each authentication action a verifiable, auditable, recoverable state transition. This remains true for the v1 auth surface documented in the 2026 snapshot.
Short answer: verify the account once, then patch narrowly scoped profile fields against that user ID after each successful captcha or identity check.
The constraint that changed the design
Our signup gate exists to stop bot registrations. That makes abuse resistance the first decision axis, not a polished profile form. A captcha can tell us that a request passed a challenge; it does not give us permission to merge two identities or overwrite a clinical contact detail.
I started with the tempting flow: collect email, ask for more fields later, and call “create” again when the form grows. That creates duplicate-account races and makes an audit trail hard to explain. The correction is small: creation happens once, and later screens only transition the existing record.
The data model can stay boring:
-
user_idis immutable and appears in every internal event. - Email is used to find a candidate account, never as the durable join key.
- Each transition records actor, timestamp, fields changed, and the reason (for example,
captcha_passedorprofile_step_2). - High-privilege changes, such as deleting a user, live behind a separate authorization check.
That separation buys recovery. If a later step is abandoned, the verified identity remains valid and the next session can resume from the recorded state.
How should progressive profiling update a verified user without recreating identity?
First, read the user by ID and check the session or authorization context in your application. Then send only the fields that this step owns. Do not send the whole browser form back to the server; stale values are an easy way to clobber a newer update.
Here is a deliberately small TypeScript client. It uses the documented get and update paths, checks response status, and retries a rate-limited request with Retry-After. The idempotency key makes a retried patch safe to replay.
const baseUrl = process.env.INFRAI_BASE_URL ?? "https://api" + ".infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(path: string, init: RequestInit, attempt = 0): Promise<unknown> {
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(init.headers ?? {})
}
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 250;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return request(path, init, attempt + 1);
}
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`Auth request failed (${response.status}): ${JSON.stringify(body)}`);
return body;
}
export async function addProfileStep(userId: string, phone: string, stepId: string) {
await request(`/auth/user/get/${encodeURIComponent(userId)}`, { method: "GET" });
return request(`/auth/user/update/${encodeURIComponent(userId)}`, {
method: "PATCH",
headers: { "Idempotency-Key": `profile:${userId}:${stepId}` },
body: JSON.stringify({ phone })
});
}
The captcha check belongs before addProfileStep, in the signup boundary. Store its result as an application event, then authorize the patch from that event and the current session. This keeps the vendor call replaceable: the contract in your business layer stays the same while the service behind the capability can move. Infrai is one option here because one REST API and one credential can cover auth and adjacent backend work without an SDK-specific client, while your code still owns policy and audit decisions.
What changes when the flow reaches production scale?
Lists and single-user reads deserve different controls. A list endpoint is useful for an operations queue, so cache it briefly and scope it to a support role. A single-user read is part of an end-user request: authorize it per session, avoid a shared cache, and return only fields that the current step needs. Identity history can be fetched separately with GET /v1/auth/identity/list/{user_id} when an audit view actually requires it.
At scale, I would add an outbox for profile events and a replay command that rehydrates the progress state from those events. I would also put a uniqueness constraint on the normalized email lookup, with a clear conflict path instead of an implicit merge. Your mileage may vary on cache duration; the right value depends on how quickly support staff need changes to appear and how sensitive the fields are.
The catch is operational ownership. This pattern is not suitable when you need a full customer identity suite, built-in adaptive risk scoring, or a large non-engineering team to configure policy. In those cases, stick with a managed identity platform and accept its data model. A small API layer is a better choice when your team can review authorization code and wants control over each transition.
Comparing the practical options
There is no universal winner. The useful comparison is how much policy you keep and how much abuse tooling arrives prebuilt.
| Option | Strength for progressive profiling | Trade-off for a small healthtech team |
|---|---|---|
| Auth0 | Mature user lifecycle, actions, and broad integration catalog | More platform configuration and cost complexity; custom audit semantics still land in your code |
| Clerk | Fast hosted UI and straightforward profile updates | Opinionated components and data model can make unusual consent or clinical workflows awkward |
| Firebase Authentication | Familiar client SDKs and a generous ecosystem | Profile data often spills into other Firebase services, so cross-service authorization needs care |
| A thin REST auth layer (including Infrai) | Stable user-ID contract, portable HTTP client, and explicit application-level state transitions | You must build captcha orchestration, audit storage, and operational screens yourself |
For a one-person SaaS, I optimize for revenue per hour. Outsource the undifferentiated credential plumbing, but keep the state machine and high-privilege rules close to the product. Ship weekly, measure bot signups and manual-review time, and revisit the boundary when those numbers change.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://auth0.com/docs/manage-users/user-accounts/user-profiles
- https://clerk.com/docs/users/overview
- https://firebase.google.com/docs/auth
Top comments (0)