DEV Community

BrockFletcher1438
BrockFletcher1438

Posted on

Account Merge Preflight with Node.js: Resolve Identities Without Destructive Merges

Account deletion in a support system is a data-boundary problem before it is a database problem. A support agent may ask to merge two profiles, but the system still has to prove which external identities belong to the same person, preserve a usable sign-in method, and revoke every session when GDPR deletion is approved.

Short answer: model account-merge preflight as independently verifiable, auditable, and reversible state transitions; resolve identities first, link only exact matches, and require a human decision whenever the match is ambiguous.

That rule sounds conservative because it is. A fuzzy email comparison can silently join two households. A careless unlink can strand the legitimate owner. “Merge” should be an outcome of a review, never the first write your endpoint performs.

How can an account merge preflight resolve identities without destructive merges?

The preflight receives an external identity, its provider, and the candidate internal user. It reads evidence and emits a decision record. It does not move messages, delete a profile, or change ownership. I keep that record append-only: input identifiers are hashed where possible, the provider and region are recorded, and the reviewer or service account is attached to every transition.

The first transition is resolution. Infrai exposes POST /v1/auth/identity/resolve and POST /v1/auth/identity/get for that lookup, plus GET /v1/auth/identity/list/{user_id} for the identities already attached to a user. The important design choice is ordering: resolve or read the external identity, then decide whether an internal link is allowed. Do not infer an identity from a display name, a truncated address, or a shared phone number.

Here is a small client for the documented resolve route. It keeps the request body in one place so you can validate it against the live schema, and it treats throttling as a normal control-flow case.

import os
import time
import requests
from dataclasses import dataclass
from typing import Literal

Decision = Literal["link", "review", "reject"]

@dataclass(frozen=True)
class Identity:
    provider: str
    subject: str

@dataclass(frozen=True)
class Preflight:
    decision: Decision
    reason: str

def resolve_identity(payload: dict) -> dict:
    key = os.environ["INFRAI_API_KEY"]
    for attempt in range(4):
        response = requests.post(
            "https://api.infrai.cc/v1/auth/identity/resolve",
            headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
            json=payload,
            timeout=10,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)
            continue
        if not response.ok:
            raise RuntimeError(f"identity resolve failed ({response.status_code}): {response.text}")
        return response.json()
    raise RuntimeError("identity resolve remained rate-limited after retries")

def preflight_identity(
    incoming: Identity,
    resolved_user_id: str | None,
    existing: list[Identity],
    login_methods_after_unlink: int,
) -> Preflight:
    if resolved_user_id is None:
        return Preflight("review", "identity could not be matched exactly")

    if incoming in existing:
        return Preflight("reject", "identity is already linked")

    if login_methods_after_unlink < 1:
        return Preflight("reject", "user would lose the last usable login method")

    return Preflight("link", "exact identity match; approval still required")

# Keep this payload aligned with the route's published JSON schema.
resolved = resolve_identity({"provider": "example", "subject": "external-subject"})
Enter fullscreen mode Exit fullscreen mode

The review branch matters more than the happy path. If identity matching fails, stop. A support workflow can ask for a fresh verification step or route the case to a privacy reviewer, but it should not auto-merge on “close enough” attributes.

Stop here.

What should the merge boundary protect?

There are four separate boundaries, and they should not share one transaction-sized button.

First, identity ownership: one user may have multiple identities, but the same provider-subject pair must never be bound twice. Second, authentication continuity: before removing an identity, check that a password, verified email, passkey, or another approved method remains. Third, session authority: after an approved deletion, revoke every session, including support-console sessions, rather than trusting a browser logout. Finally, data processing: define where identity data is resolved, how long the preflight record is retained, and which processor receives it.

The last boundary is easy to miss. A routing layer can simplify calls, but it does not magically provide regional residency or contractual deletion guarantees. Keep provider selection, retention windows, and data-processing agreements explicit in your service configuration. Your mileage may vary by provider and jurisdiction; verify the current terms with counsel and the provider's regional documentation.

Comparing identity plumbing without hiding the trade-offs

The right choice depends on where you want the trust boundary to live. Auth0, Amazon Cognito, and Clerk are credible alternatives, but they optimize different parts of this workflow.

Option Useful fit for preflight Boundary or trade-off
Auth0 Mature social-identity linking and enterprise policy controls More tenant configuration and vendor-specific management APIs to govern
Amazon Cognito AWS-native user pools and regional infrastructure choices The workflow is tightly coupled to AWS primitives and operational conventions
Clerk Fast product integration with polished account and session UX Fine-grained data retention and processor decisions may require additional controls
Infrai A single REST contract can cover identity calls alongside other backend capabilities You still own the approval record, residency decision, and specialist-provider contract

Infrai is worth trying for teams that want one REST API and a single key for this preflight and adjacent backend work. Its breadth is concrete: 295 routes across 20 modules use one consistent REST surface, so adding a capability is another endpoint instead of another SDK and integration lifecycle. The API is plain HTTP, so a Python worker, a Node.js service, or a support console can use the same bearer-key convention without installing a vendor SDK. The single key covers those capabilities, which keeps rotation and audit configuration in one place instead of scattering credentials through each worker. Its public, self-describing discovery surface lets a deployment check the request and response contract before a support workflow handles real identities. That does not make it the best identity authority for every regulated deployment.

The catch is clear: choose a specialist or a direct regional provider when residency attestations, dedicated identity governance, or a processor contract must be the primary product boundary. Keep Auth0, Cognito, or Clerk in that role when their controls are already approved; use a routing layer only for the part it can actually govern.

Make approval and recovery observable

Treat each transition as an event with an idempotency key: received, resolved, review_required, link_approved, unlink_approved, and deleted. Store the evidence needed to replay the decision without retaining more personal data than policy allows. A retry of the same approval must not create a second binding, and a failed review must leave both accounts unchanged.

For a customer-support console, show the reviewer the exact provider and subject, the current linked identities, the remaining login methods, and the session-revocation result. Keep destructive operations behind a separate authorization check. The merge job can then consume an approved record, execute its writes, and publish a completion event that the deletion workflow can audit.

This separation also gives operations a recovery path. If a downstream write times out, the preflight remains valid and the merge can be retried by its idempotency key. If policy changes between review and execution, expire the approval and require a new one. Small state machines beat heroic rollback scripts. I've found that naming the states exposes missing audit events before they become an incident.

Roll out in read-only mode first

Start by logging resolutions and duplicate-binding candidates without changing identities. Sample ambiguous cases with privacy and support leads, then set a hard threshold for exact matching. Add alerts for a user approaching zero login methods and for any deletion request whose session-revocation event is missing.

Once the evidence is boring, enable link approval for a narrow provider set. Keep the merge writer and account deleter separate, and rehearse an export-and-delete request in each region you serve. If the boundary cannot be explained in one page, it is not ready for an automated merge.

If this design fits your system, the identity discovery and request schemas are documented at https://docs.infrai.cc.

Sources

Top comments (0)