Progressive Profiling: Update Verified Users Without Recreating Their Identity
Short answer: progressive profiling should update the record addressed by an immutable user ID, preserve verified identifiers as identities, and record each accepted change as a separately authorized, auditable state transition; it should never create a replacement user merely because a profile field or recovery address changes. For a healthtech sign-up flow, that rule matters more than collecting every field on day one. A patient may begin with an email and password, then add a display name, recovery method, or other application-owned attributes later. The authentication record answers “who is this?” while the profile answers “what do we currently know about this user?” Combining those questions makes recovery dangerous: a changed email can accidentally become a changed person. What does progressive profile retention actually cost? The useful cost model is not a vendor price table. It is the amount of state the team must retain, reconcile, authorize, and eventually delete. Let U be users, P the mutable profile fields stored per user, and E the accepted profile transitions. Current profile storage grows roughly with U x P; the audit history grows with E. In a system where profiles change repeatedly, E becomes the dominant retention term. Provider calls and invoices still matter, but they aren't the hard part of explaining a patient's account history to a security reviewer. One current row plus one immutable audit event per accepted transition is a tractable design. The event needs the stable user ID, actor, time, operation, previous version, resulting version, and a correlation or idempotency key. Sensitive values need not be copied wholesale into the event. A field name, classification, and integrity-protected reference may provide the required evidence with less exposure; the exact retention period must come from the organization's legal and compliance analysis, not from a generic authentication recipe. This changes the dominant term by separating operational state from evidence. The current row stays bounded while the event stream grows predictably, so older events can move into a retention tier with stricter access instead of forcing every sign-in read through an ever-growing document. Keep enough history to reconstruct authorized transitions and investigate account recovery. Deliberately stop keeping password material, reset secrets, full request bodies, and redundant copies of sensitive profile values. The catch is that aggressive minimization can reduce forensic detail when something goes wrong, so deletion schedules must be approved alongside incident-response and healthcare compliance requirements. Short-lived recovery artifacts are different from identity history. How should progressive profiling update a verified user without recreating identity? Treat the user ID as the aggregate key. Email is a lookup attribute and, once verified, an attached identity; it is not the primary key for profile writes. Read the user by ID, authorize the requested transition, compare the caller's expected version, apply only allowed fields, and append the audit event in the same business transaction. A stale request should receive a 409 Conflict, a caller without the required privilege should receive 403 Forbidden, and a structurally valid but disallowed transition can receive 422 Unprocessable Entity. Those are business-layer choices, not claims about a provider's response catalog. The email-change path deserves its own state machine. A request to replace a verified email should not overwrite it immediately or insert a second user. Record a pending change, prove control of the new address, apply the verified transition to the existing user ID, and decide separately whether policy requires a notification or fresh authentication on the old channel. Password-reset possession also must not silently authorize high-risk profile changes. OWASP recommends consistent responses for password recovery requests so that the flow does not disclose whether an account exists. Consider two concurrent updates that both read profile version 17. The first adds an optional attribute and commits version 18. The second, perhaps an account-recovery operation, must not overwrite version 18 from its stale snapshot. An optimistic version check rejects it; the caller rereads, reauthorizes, and submits a new transition. An idempotency key solves a different problem: retrying the first accepted command returns the same logical outcome rather than appending a second event. You need both controls. Exactly-once delivery is rarely the primitive available at a network boundary, but exactly-once business effect is an achievable invariant when deduplication and version checks are enforced together. No shortcuts. Read before writing. The following runnable Go program retrieves the current user by stable ID before the business transaction begins. It deliberately does not guess an update payload: the application should load the current request schema from discovery, validate that schema at its integration boundary, and then send the permitted patch. The example uses an environment variable for the key, an explicit method, bounded retries for 429 Too Many Requests, Retry-After when supplied, and response-status checks. The optimistic version check, deduplication record, profile update, and audit append still belong in one atomic business transaction. package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" ) func main() { key := os.Getenv("INFRAI_API_KEY") baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/") if key == "" || baseURL == "" || len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY and INFRAI_BASE_URL, then pass one user ID") os.Exit(2) } path := strings.Replace("/v1/auth/user/get/{user_id}", "{user_id}", url.PathEscape(os.Args[1]), 1) endpoint := baseURL + path client := &http.Client{Timeout: 15 * time.Second} for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+key) resp, err := client.Do(req) if err != nil { panic(err) } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { panic(readErr) } if resp.StatusCode == http.StatusTooManyRequests { delay := time.Duration(1
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to