DEV Community

MordecaiNilsson7582
MordecaiNilsson7582

Posted on

5 Postgres Invariants for Subscriber Email Changes and Durable Account History

Short answer: treat a subscriber's email as a verified, replaceable login attribute, keep an immutable internal account ID as the ownership key, and move every dependent record through one controlled transition. For a media subscription service, that choice preserves entitlements, invoices, preferences, and reading history when an address changes, while leaving room to migrate away from a managed identity provider later.

The uncomfortable trade-off is duplication. The application must own a small identity ledger instead of trusting the provider's current email field as its subscriber model. That extra state is justified because an email address can change ownership, spelling, verification status, or provider representation; a paid account's continuity cannot inherit all of those changes by accident.

This is a data-lifecycle problem before it is a login-screen problem.

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

Start with one invariant: account_id never means email. Give each subscriber an opaque internal identifier and make it the foreign key used by subscriptions, payment references, newsletter choices, saved articles, support cases, and audit entries. Store the normalized email in a separate credential or contact record whose lifecycle can be changed without moving ownership of those rows.

For an illustrative account, account_id = 7f3c... can own a monthly publication entitlement while old@example.test is replaced by new@example.test. The entitlement row does not move. Neither does the reading history. Only the verified login attribute changes, and the ledger records the transition from the old value to the new one. The ellipsis here is sample notation, not a prescribed identifier format.

Do not expose the immutable ID as evidence that a person owns the account. It is a database key, not an authenticator.

This boundary also clarifies deletion. Removing an old email after the retention period should not delete the subscriber record by cascade, while deleting the subscriber under the service's policy should deliberately remove or anonymize dependent data. Those are different operations and deserve different transaction paths. If a schema cannot express that distinction, migration code will eventually have to guess, and guessing about paid entitlements is a poor control.

Record Stable owner key Mutable field Failure if email is the key
Subscription entitlement account_id Plan and status An address edit can orphan access
Login credential account_id Verified email An old address can remain authoritative
Reading preferences account_id Topics and delivery settings Preferences can split across accounts
Identity audit entry account_id Before/after email references Investigators lose the change chain

The catch is real: this design is not suitable for a throwaway mailing list with no account history, entitlement, or recovery workflow. A list-specific subscriber key may be enough there. Once money, licensed content, or durable preferences attach to the identity, use the internal account boundary.

2. Make the replacement one transaction with two proofs

An email replacement needs proof of the current account session and proof that the applicant controls the new address. The first protects the existing subscriber; the second prevents a typo or an unowned mailbox from becoming the next login identifier. OWASP's Authentication Cheat Sheet recommends reauthentication for sensitive account changes and describes generic error responses that avoid disclosing whether an account exists.

Model the operation as a state transition rather than a direct profile edit. A pending request belongs to account_id, carries the candidate email, has an expiry, and can be consumed once. When confirmation arrives, lock the relevant account and email-claim rows, verify that the request is still valid, assert uniqueness under the same normalization rule used at signup, write the new verified claim, retire the former claim, append an audit event, and commit. One boundary. If any assertion fails, no ownership state changes.

The following Python sketch leaves transport and persistence adapters abstract on purpose. Its useful part is the ordering and the fact that the authorization decision is repeated inside the transaction rather than trusted from an earlier screen.

def confirm_email_change(db, account_id, request_id, token_digest, now):
    with db.transaction() as tx:
        request = tx.lock_change_request(request_id)
        account = tx.lock_account(account_id)

        if request.account_id != account.id:
            raise InvalidChangeRequest()
        if request.consumed_at is not None or request.expires_at <= now:
            raise InvalidChangeRequest()
        if not constant_time_equal(request.token_digest, token_digest):
            raise InvalidChangeRequest()

        candidate = normalize_email(request.candidate_email)
        tx.assert_email_available(candidate, excluding_account=account.id)
        previous = tx.get_verified_email(account.id)
        tx.replace_verified_email(account.id, candidate, verified_at=now)
        tx.consume_change_request(request.id, consumed_at=now)
        tx.append_identity_event(
            account_id=account.id,
            event_type="email_changed",
            previous_email=previous,
            current_email=candidate,
            occurred_at=now,
        )
Enter fullscreen mode Exit fullscreen mode

Normalization policy must be explicit and stable. I'm not sure there is a universal provider-independent rule beyond the conservative transformations your service documents; the decision should come from observed duplicate-account risk and mailbox semantics, then be covered by fixtures before migration. Do not silently introduce a more aggressive rule during the provider move. That can merge two previously distinct claims.

3. Rotate recovery and sessions at the identity boundary

Changing the row is not enough. A subscriber who replaces an email because the old mailbox is compromised expects the old recovery path to stop working, yet a blanket logout with no explanation can turn a defensive action into a support incident.

Use the completed change event as the boundary for revoking recovery tokens tied to the previous address and invalidating sessions according to the service's threat model. OWASP calls for session invalidation or token rotation after reauthentication and risk events. A practical policy can preserve the just-reauthenticated session long enough to show a confirmation while revoking other sessions, but that is a product and risk decision, not a database default.

Keep responses generic at public endpoints. “If the request is valid, check the destination address” does not reveal whether a media subscriber exists, while internal logs can retain a reason code such as change_request_expired or candidate_already_claimed. The public and operational audiences need different levels of detail.

Short responses outside. Precise reasons inside.

Do not automatically transfer an account merely because a new provider reports the same email text. Provider assertions, local verified claims, and account recovery are separate trust paths. Bind a provider subject to the immutable account only after the migration flow has authenticated the existing subscriber or completed a deliberately designed recovery challenge.

4. Preserve evidence without turning the audit log into a second profile

An identity ledger should answer a narrow set of questions: which account requested a change, which verified claim became active, when the transition committed, which session or actor authorized it, and which policy version evaluated it. It should not become an unrestricted copy of every request header and token. Secrets and raw confirmation tokens do not belong there.

This limit matters in a media service because identity data spreads easily: newsletter tooling, analytics exports, payment reconciliation, and support systems may all cache an email. The database transaction can preserve account ownership while those downstream copies remain stale. Treat propagation as a separate, idempotent workflow keyed by the immutable account ID and the identity-event ID. A consumer that sees the same event twice should converge on the same current contact value rather than create another subscriber.

Audit retention deserves an explicit decision table, even if the final durations are set elsewhere:

Evidence Keep in identity ledger? Reason
Account ID and event type Yes Reconstructs continuity
Event time and policy version Yes Explains which rule ran
Raw verification token No Adds credential exposure without audit value
Full request headers Usually no Broad collection with weak relevance
Prior email Policy-dependent Useful for disputes, but increases retained personal data

There is no magic retention number in this design. Legal obligations, dispute windows, and privacy policy have to supply it. Your mileage may vary by market and subscription model, so make the value configurable and test deletion as seriously as insertion.

5. How can a migration prove continuity before changing identity authority?

Provider migration should be boring. Export identities into a staging model keyed by the old provider subject, map each subject to exactly one internal account_id, and classify every exception before the new login path becomes authoritative. Duplicate normalized emails, missing verification state, multiple provider subjects linked to one account, and paid entitlements with no identity mapping are named failure modes; none should be resolved by first-row-wins logic.

Run the old and new mappings in comparison mode on a controlled sample, without allowing the comparison path to mutate identity. The acceptance check is not “the row counts match.” It is that each active entitlement resolves to the same immutable account, each active verified email has one intended owner, old recovery artifacts cannot authorize a post-change account, and downstream consumers can replay identity events without duplicating subscribers.

Then roll out in compact stages: freeze schema changes that affect identity semantics, backfill mappings, validate invariants, switch a limited cohort, monitor rejection reason codes and support contacts, and expand only after the exception queue is understood. Keep rollback about authority routing, not destructive data reversal; the internal account ID and append-only change evidence should remain stable in either direction.

Stick with the managed provider's native identity record when it already exposes an immutable subject, preserves the verification state you need, and there is no credible migration or multi-provider requirement. Owning another ledger has operational cost: schema reviews, retention jobs, transaction tests, access control, and incident procedures. For a paid publication planning a provider exit, those costs buy a boundary the service can verify. For a small newsletter without durable account state, they may buy paperwork.

References

Top comments (0)