DEV Community

loganpierce2073
loganpierce2073

Posted on

Node.js Marketplace Password Reset Email Deliverability with 5 Branded Suppression States

TL;DR: Treat a marketplace password-reset email as a five-state workflow: requested, eligible, submitted, observed, and suppressed. A Node.js service should make the eligibility decision from a versioned suppression snapshot, render an immutable branded template revision, and enqueue one logical send before any transactional email API call. Later delivery or bounce evidence advances the record; it never rewrites the original decision. This gives operators a defensible answer when a buyer or seller cannot recover an account without pretending that API acceptance proves inbox placement.

The trade-off is deliberate. Persisting state transitions and their evidence costs more engineering effort than calling a mail client in the request handler, but compliance review requires reproducible decisions, while account recovery requires retries that do not create a second logical action. The exactly-once property belongs at the database boundary. Transport remains uncertain.

How should Node.js handle branded password reset email deliverability?

A single sent boolean collapses facts with different meanings. A recovery request can be valid while its recipient is already suppressed; a mail system can accept a submission without establishing where a mailbox placed it; and a later bounce can justify withholding the next recovery message without changing what was known during the first attempt. Those distinctions matter when the address belongs to a seller account with unsettled orders or a buyer awaiting a refund, because the audit question is usually not “did some function return success?” but “which policy and evidence produced this action?” The same row cannot faithfully answer both questions after a later outcome arrives: mutating sent to bounced destroys the earlier submission fact, while leaving it as sent hides the evidence that must govern the next attempt. An append-only transition preserves both.

The five states are intentionally asymmetric:

State What it establishes Required evidence
requested A recovery workflow was opened Stable challenge ID, account reference, request time
eligible The address was not suppressed under the evaluated policy Policy version, suppression snapshot version, recipient digest
submitted One logical send was handed to the transport adapter Outbox ID, template revision, adapter receipt when available
observed A later authenticated event was retained Source event ID, receipt time, payload digest, classifier version
suppressed Future sends are withheld for a recorded reason Reason code, effective time, originating observation

These are business states, not a transcription of any provider's event names. The separation keeps historical evidence stable if the transport changes, and it forces the application to represent absence: a suppressed attempt still receives a decision record even though no message is submitted.

Absence needs evidence.

Credential terminology also needs precision. RFC 6238 defines TOTP through HOTP with a time value and a shared secret. A random, expiring reset-link token is therefore not automatically a TOTP merely because it is short-lived. Store a digest or reference for a recovery secret, never the usable secret in a general-purpose mail ledger, analytics field, or rendered-preview URL.

Model transitions before choosing a transactional email API

The useful invariant is one logical decision per recovery challenge and template revision. In a single database transaction, insert the decision if it does not exist, evaluate the current suppression projection, and create an outbox row only for an eligible recipient. A unique constraint arbitrates concurrent requests. A prior read followed by an insert does not.

Small detail, large consequence.

The HTTP response should remain generic so it does not disclose whether an account or address exists. The mail worker then consumes the committed outbox independently. If its network call times out, it reconciles the same item and any stored adapter receipt; it does not mint a replacement decision simply because the remote result is unknown.

The following Go types define the contract that the Node.js handler and worker need to share. The code uses no provider SDK types, because those types should not become the vocabulary of an audit record.

package recovery

import (
    "context"
    "errors"
    "time"
)

type State string

const (
    Requested  State = "requested"
    Eligible   State = "eligible"
    Submitted  State = "submitted"
    Observed   State = "observed"
    Suppressed State = "suppressed"
)

type Decision struct {
    ChallengeID       string
    AccountID         string
    RecipientDigest   string
    TemplateRevision  string
    PolicyVersion     string
    SuppressionVersion string
    State             State
    ReasonCode        string
    DecidedAt         time.Time
}

type Store interface {
    ReserveOnce(ctx context.Context, d Decision) (Decision, error)
}

func Reserve(ctx context.Context, store Store, d Decision) (Decision, error) {
    if d.ChallengeID == "" || d.AccountID == "" || d.RecipientDigest == "" {
        return Decision{}, errors.New("missing recovery identity")
    }
    if d.TemplateRevision == "" || d.PolicyVersion == "" || d.SuppressionVersion == "" {
        return Decision{}, errors.New("missing decision evidence")
    }
    return store.ReserveOnce(ctx, d)
}
Enter fullscreen mode Exit fullscreen mode

ReserveOnce must return the previously committed decision on a retry. It must also fail closed when the suppression projection cannot be evaluated, because “lookup unavailable” is not evidence that the recipient was eligible. This choice can delay a reset email during a dependency failure, so the operational design needs a visible queue age and a verified alternate account-recovery path; silently bypassing suppression would make the compliance policy conditional on system health.

Freeze the branded artifact that was actually approved

Branding is part of the controlled message, not decoration applied after approval. Assign an immutable revision to the subject, HTML, plain-text alternative, link host, legal text, and optional image behavior. A mutable label such as password-reset-latest cannot show what a recipient was meant to receive after a copy or domain change.

Test the exact revision with three fixtures: an ordinary marketplace account, the longest shop and display names allowed by local validation, and a rendering with optional remote imagery absent. Verify both body formats, the expected link host, and that the recovery instruction remains understandable without images. Three fixtures are a review floor, not a claim of exhaustive coverage.

The reset secret should be introduced only at the narrow rendering boundary. Logs can retain the challenge ID, template revision, and a keyed recipient digest, but the reset URL and address should not flow into routine tracing attributes. A digest is still pseudonymous data; its key needs controlled access and rotation, and retention should follow the applicable policy rather than the convenience of indefinite debugging.

This boundary also clarifies the role of a transactional email API. An application-level adapter should translate each external response into the marketplace's limited states. The durable record says what the marketplace decided and observed; it does not inherit a transport schema as its permanent compliance model.

Make bounces change future decisions, not past facts

Bounce ingestion is a new write path. Retain the authenticated source event, deduplicate it by source and stable event identifier, record its receipt time and payload digest, and interpret it with a versioned classifier. The raw observation and the interpretation are separate records so a corrected classifier can be replayed without altering the evidence received.

A bounce classified under policy as an invalid recipient can advance the suppression projection used by later recovery attempts. A temporary or ambiguous failure needs a distinct reason because treating every failure as permanent can block a legitimate recovery. Conversely, deleting the original submission record after suppression would erase the chain that explains why the projection changed.

Events may be duplicated or arrive out of order. Define deterministic precedence for projection updates and apply deduplication transactionally; otherwise, a delayed event can move an address back to an older interpretation. Keep the raw event, interpreted fact, and current projection distinct. More rows are cheaper than an audit trail whose meaning changes whenever the classifier does.

Order is evidence too.

Inbox placement belongs outside this state machine. A controlled mailbox cohort can record where specified test messages appeared at specified times, but that observation cannot establish placement for every buyer and seller. Report submission evidence, delivery observations, cohort placement, and completed password resets as separate measures. None is a synonym for another.

Roll out through replayable evidence

Begin in observation mode. Write the five-state records alongside the existing recovery path, compare decisions by reason code, and do not enforce the new suppression projection until mismatches are understood. Then make the outbox the only source of transport work while preserving the generic user-facing response.

Enable enforcement first for one explicitly classified invalid-recipient reason. Retain a verified route for an account holder to replace an unreachable address. Expand only when duplicate events, uncertain submissions, projection lag, and joins from challenge to outcome are observable; rollback should stop new enforcement without deleting decisions or bounce evidence.

The acceptance test is concise: submit the same recovery command twice, ingest the same bounce observation twice, and rebuild the suppression projection from append-only records. The result should contain one logical decision, one interpreted observation, and the same final projection. That is the compliance guarantee. Branded markup and transport adapters can change without changing the history.

Sources

Top comments (0)