DEV Community

RonanHalewood782
RonanHalewood782

Posted on

Email Continuity in Python: Immutable IDs vs New Accounts (Choose IDs for Address Changes)

Short answer

Short answer: Keep one immutable account ID and treat an email address as a verified, replaceable login attribute; creating a new account on every address change makes recovery and device-risk decisions unreliable. For a media service, this means a fingerprint can raise risk without silently splitting a subscriber's history.

I arrived at this choice by looking at the failure that hurts support teams most: a reader changes jobs, loses an old mailbox, and signs up with the new address. The database now has two identities, two recovery histories, and a risk model that sees the same laptop as two unrelated people. The tempting fix is an email-keyed account table. It is quick in a notebook. It is painful in production.

Why does email continuity matter when a media login changes address?

Email is a useful contact and a familiar login hint, but it is not a durable identity key. Addresses are recycled, aliases vary by provider, and a shared family inbox can be controlled by more than one person. OWASP's Authentication Cheat Sheet recommends treating account recovery as a sensitive authentication flow, with consistent verification and notification rather than a casual profile edit.

A practical record separates concerns:

Field Purpose Change policy
user_id Stable ownership of subscriptions, history, and risk events Never changes
email Login and notification address Replace only after verification
email_verified_at Evidence for the current address Reset on replacement
recovery_state Pending, verified, or locked recovery path Audited transition
device_fingerprint Signal for login risk, not identity Rotate or expire by policy

That split prevents a fingerprint from becoming a back door. A familiar device can lower a score, but it cannot approve a new email by itself.

The account-recovery path should be explicit: authenticate with an existing factor, verify the new address, notify the old address when possible, revoke sensitive sessions, and record the event. If the old mailbox is unavailable, use a separately protected recovery factor or a manual review queue. Do not make “the device looks familiar” equivalent to proof of ownership.

What should a Python implementation compare before creating a new account?

Start with an identity lookup, then decide whether the request is an address replacement, a merge candidate, or a genuinely new registration. This small service keeps the decision testable and independent of a particular identity vendor.

from dataclasses import dataclass
from enum import Enum


class Decision(str, Enum):
    UPDATE = "update_existing"
    NEW = "create_new"
    REVIEW = "manual_review"


@dataclass
class LoginContext:
    user_id: str | None
    old_email_verified: bool
    new_email_verified: bool
    device_risk: float
    recovery_factor_ok: bool


def choose_account_path(ctx: LoginContext) -> Decision:
    if ctx.user_id is None:
        return Decision.NEW
    if not ctx.new_email_verified:
        return Decision.REVIEW
    if ctx.device_risk >= 0.8 and not ctx.recovery_factor_ok:
        return Decision.REVIEW
    if ctx.old_email_verified or ctx.recovery_factor_ok:
        return Decision.UPDATE
    return Decision.REVIEW
Enter fullscreen mode Exit fullscreen mode

The important detail is the order. A high risk score does not erase the existing account, and a low score does not skip email verification. I once started with a single boolean called trusted_device; it collapsed “known browser” and “recovery authority” into one bit. That made an eval look good while the security review found the gap. The revised rule has a boring name and a useful audit trail.

How do the two account models behave under recovery pressure?

The comparison is less about database taste than about what survives an incident.

Model Address change Recovery continuity Device-risk interpretation Main trade-off
Email as primary key Often inserts a second row History can split or require a risky merge Same device may map to two accounts Simple first release, expensive exceptions
Immutable ID plus mutable email Updates one verified attribute Subscription and recovery events stay attached Fingerprint remains a signal on one identity Requires explicit verification and audit states

The immutable-ID model is the better default for a media product with long-lived subscriptions. It also makes deletion and export requests easier to reason about because records have one ownership anchor. It does add work: you need unique-email rules, pending-change storage, notifications, rate limits, and an operator workflow for edge cases.

The catch is important. If your product intentionally treats every address as a separate tenant, or if legal policy requires hard identity separation, do not merge accounts automatically; keep the new-account model and make the boundary visible to users. Likewise, a service with no recovery factor and no support capacity may need a narrower feature than “change email anywhere.”

Which tests expose a broken email-change flow?

I use an eval harness before wiring the handler to production storage. The cases are deliberately unglamorous: a verified old address, an unverified new address, a reused address belonging to another user, a high-risk device, and a recovery link that is replayed after the session is revoked. Each case should produce a decision, an event, and a notification outcome.

Measure false merges and false splits separately. A false merge joins two people; a false split strands a legitimate subscriber. Track time to recover, percentage of changes completed with the old factor, support escalations, and how often device risk changes the path. Keep a small, labeled corpus of recovery attempts with outcomes, replay it after every change to the risk threshold, and inspect disagreements instead of averaging them away; a model that improves aggregate accuracy can still make the one high-value subscriber impossible to recover. Your mileage may vary on thresholds because media audiences and fraud pressure differ; publish the threshold policy beside the code so a later model update is reviewable.

Prompt-cost awareness matters here too. For an AI-assisted support triage tool, send structured event fields to the model instead of a whole account transcript. Keep deterministic identity rules in Python, and let the model explain a flagged case rather than decide ownership. That keeps the notebook-to-prod jump small and makes regression tests cheap.

Ship the schema split first, then dual-write events while reads still use the old path. Backfill stable IDs with a reviewed mapping. Next, put email replacement behind a feature flag for internal accounts, and replay the eval corpus on every rule change. Only then open the flow to a small percentage of users.

Ship it slowly.

Keep an audit event for each state transition: who requested the change, which factor verified it, the risk score at the time, and which sessions were revoked. Never log the verification token itself. Alert on bursts of changes from one device or IP, but avoid treating an IP address as identity evidence.

The decision rule is compact: preserve the account when ownership is verified; create a new account when there is no trustworthy link; route ambiguity to review. That is enough structure to keep email continuity intact without pretending that a device fingerprint proves who is behind a login.

Sources

Top comments (0)