DEV Community

HieronymusFox1257
HieronymusFox1257

Posted on

Node.js Email Changes — Subscriber Identity Design With Immutable Account Keys

Short answer: subscriber identity design should let email changes preserve account continuity by keeping email mutable, anchoring ownership to an opaque subscriber ID, and revoking every existing session after the new address is verified.

For a media subscription sold through an e-commerce account, this choice protects two things that pull in opposite directions. The reader should keep access to paid issues and purchase history after changing an address. The operator should also be able to delete the account and terminate every session for a GDPR request. Using email as the database key makes both jobs harder because a contact field has quietly become identity, a login alias, and a foreign key.

The deciding constraint is session security versus friction. A low-risk profile edit should not erase entitlements. An identity change should not leave old sessions trusted forever either.

Replace the email-shaped mental model

The before model is easy to picture: email -> subscriber -> subscription. An email appears unique, so orders, newsletter preferences, saved articles, and sessions all point to it. Then alex@old.example becomes alex@new.example. Every dependent record must be rewritten perfectly, caches may still hold the old value, and an old bearer session may continue authorizing requests under assumptions that are no longer true.

The after model has one extra hop: credential or verified email -> principal ID -> account data. The principal ID is generated once and never recycled. Email belongs in a separate alias record with verification state and timestamps. Orders, entitlements, consent records, and sessions refer only to the principal ID. This is a small schema decision with a large operational payoff — changing an address updates an alias, while deleting a principal gives the erasure workflow one stable join key.

Keep it boring.

Here is the boundary I would teach in an architecture review. Authentication proves control of a credential. Identity selects the durable principal. Authorization reads the current entitlements attached to that principal. Contact delivery uses the currently verified email. Those concerns can share a transaction, but they should not share a primary key.

Concern Stable key Mutable state Failure to prevent
Subscriber account principalId status, profile a new email creating a second paid account
Login alias aliasId normalized email, verification an unverified address taking over login
Subscription principalId plan, renewal state access disappearing after an address edit
Session sessionId plus principalId expiry, revoked time an old device surviving a security event
Erasure job principalId phase, audit timestamps partial deletion caused by an outdated email

Normalization also needs restraint. Store the exact address the subscriber entered for display and delivery, plus a separate normalized value for comparison. Don't invent provider-specific rules such as removing dots or plus tags across every domain. If the business needs stronger equivalence, document the rule, test it against the mail systems in scope, and keep the original value.

How should subscriber identity handle email changes without breaking account continuity?

Start with proof, not mutation. Require a recently authenticated session before accepting a sensitive account change, send verification to the proposed address, and leave the current verified address active until that proof succeeds. OWASP's Authentication Cheat Sheet recommends reauthentication after risk events and rotating or invalidating sessions after reauthentication. An email change belongs in that category because it can alter both recovery and future login.

Once proof succeeds, commit the alias transition and the security transition together. Mark the former alias inactive, promote the verified alias, increment a principal-wide session version, and record the event. Requests carrying an older version stop at the next authorization check. This avoids a race in which the UI says the address changed but another device retains a session minted under the earlier account state.

Make that atomic.

There is real friction here. Revoking all sessions means the person must sign in again on the phone, tablet, and television app. Keeping sessions alive feels smoother, but it weakens the value of reauthentication when the old inbox or a device may be compromised. For a media account with stored payment details or purchases, full revocation is the clearer default. A low-value newsletter profile with no authenticated entitlements may justify a lighter rule; the policy should follow the risk, not a universal ceremony.

Account deletion uses the same principal boundary but a different state machine. First mark the principal deleting so new sessions and email changes cannot begin. Revoke sessions. Then process dependent records by principalId, separating data that must be erased from records that must be retained under an applicable obligation. Only the application's legal and data owners can define those retention rules; I'm not sure a generic authentication design can answer that question without a documented data inventory and counsel for the service's jurisdictions.

This ordering matters. Picture the subscriber starting deletion in a browser while a verification link opens on a phone. The browser marks principal p_4821 as deleting; milliseconds later, the phone attempts to promote a pending alias. If both operations first lock the same principal and require active status, only one state transition can win. The deletion path then revokes every session version and walks purchases, entitlements, preferences, and aliases by p_4821. If code instead deletes the login alias first and later tries to find purchases by email, it has destroyed its own lookup key. If it revokes only the browser session that submitted the request, the phone or television may remain active. The stable principal ID prevents the first error. A principal-wide revocation marker prevents the second.

Copy the invariant into Node.js code

The useful example is not an endpoint catalog. It is one transaction that preserves the invariant: an active principal has at most one current verified email, and any successful change advances the session version. The surrounding mail sender can create a pending verification record before this function runs.

type Principal = {
  id: string;
  status: "active" | "deleting" | "deleted";
  sessionVersion: number;
};

type EmailAlias = {
  id: string;
  principalId: string;
  address: string;
  normalizedAddress: string;
  verifiedAt: Date | null;
  active: boolean;
};

type VerifiedChange = {
  principalId: string;
  pendingAliasId: string;
  verifiedAt: Date;
};

interface Transaction {
  lockPrincipal(id: string): Promise<Principal | null>;
  lockAlias(id: string): Promise<EmailAlias | null>;
  deactivateEmailAliases(principalId: string): Promise<void>;
  activateVerifiedAlias(aliasId: string, verifiedAt: Date): Promise<void>;
  setSessionVersion(principalId: string, version: number): Promise<void>;
  appendSecurityEvent(event: {
    principalId: string;
    type: "email.changed";
    occurredAt: Date;
  }): Promise<void>;
}

async function commitVerifiedEmailChange(
  tx: Transaction,
  change: VerifiedChange,
): Promise<{ principalId: string; sessionVersion: number }> {
  const principal = await tx.lockPrincipal(change.principalId);
  const alias = await tx.lockAlias(change.pendingAliasId);

  if (!principal || principal.status !== "active") {
    throw new Error("ACCOUNT_NOT_ACTIVE");
  }
  if (!alias || alias.principalId !== principal.id) {
    throw new Error("VERIFICATION_NOT_OWNED");
  }
  if (alias.verifiedAt) {
    throw new Error("VERIFICATION_ALREADY_CONSUMED");
  }

  const nextSessionVersion = principal.sessionVersion + 1;

  await tx.deactivateEmailAliases(principal.id);
  await tx.activateVerifiedAlias(alias.id, change.verifiedAt);
  await tx.setSessionVersion(principal.id, nextSessionVersion);
  await tx.appendSecurityEvent({
    principalId: principal.id,
    type: "email.changed",
    occurredAt: change.verifiedAt,
  });

  return { principalId: principal.id, sessionVersion: nextSessionVersion };
}
Enter fullscreen mode Exit fullscreen mode

The caller must run those operations in a database transaction with locks or equivalent serialization. A unique constraint on the normalized active alias protects against two principals claiming the same login address, while a second constraint prevents multiple active aliases for one principal. The exact constraint syntax depends on the database, so the portable requirement is more important than pretending one snippet fits every engine.

At request time, compare the session's embedded version with the current principal version. A mismatch returns an authentication failure and directs the client through sign-in again. Avoid logging the raw email, verification secret, or session token. Log opaque IDs and state transitions instead: principalId, aliasId, previous and next session versions, event type, result, and a correlation ID. That gives support and security teams a crisp trail without turning observability data into another store of personal contact details.

No raw email.

Watch four signals after deployment: email-change attempts, verification completions, rejected stale sessions, and duplicate-alias conflicts. Alert on changes in rates, not on a single expected rejection. A stale-session rejection immediately after a successful change is the design working.

What about support recovery and shared inboxes?

The first objection is recovery: what if the subscriber has lost the old inbox? Requiring confirmation from both old and new addresses can permanently trap a legitimate owner, while trusting only the new address can turn a stolen session into takeover. The practical boundary is a separate recovery path with stronger evidence, rate limits, human review where appropriate, and the same session revocation after success. Recovery should never be a hidden flag that lets support overwrite an email without producing a security event.

The catch is that this design is not suitable when the service intentionally treats an inbox as a shared team identity. In that model, changing the email may actually mean replacing the group, and an individual principal plus explicit memberships is usually a better fit. Likewise, stick with an external identity provider's immutable subject identifier when authentication is delegated; map that subject to the local principal and keep email descriptive. Do not silently fall back to email when the subject is unavailable.

The second objection is customer experience. Must every email edit sign out every device? No single rule fits every risk tier. If sessions protect only free public preferences, revoking the current session after reauthentication may be enough. If they expose paid media, purchase records, recovery controls, or stored payment access, principal-wide revocation is easier to reason about and explain. Make the sign-out visible in the confirmation screen, preserve the subscriber's entitlements under the same principal, and measure failed sign-ins after rollout. Security is the default; surprise is optional.

Before shipping, test the awkward sequences: two verification links opened in reverse order, a deletion request racing an email change, an expired verification, concurrent claims on one normalized address, and a request from an old session immediately after commit. The expected outcome should be deterministic for each case. That is where the schema earns its keep.

References

Top comments (0)