Short answer: for a Next.js or Node.js marketplace welcome email, keep the transactional template in the marketplace's versioned repository, render an immutable artifact before sending, and record each delivery transition in an append-only ledger. The dominant cost is usually retained evidence, not the API call: one logical notice can leave a template revision, rendered body, attachment, provider event payloads, and repeated status snapshots. Store one canonical artifact plus normalized events, then expire redundant raw payloads under a documented retention schedule.
This is the least complex design that can answer the question an auditor will eventually ask: what exactly did this seller receive, under which policy version, and what evidence supports the delivery status? Polling remains useful, but it is reconciliation, not the source of truth. A suppression hit is a terminal business outcome, not a transport exception to retry until it disappears.
How should a Next.js marketplace handle welcome email delivery and suppression?
Consider a marketplace that must notify a seller when its payout account is restricted. An illustrative notice contains a 45 KB rendered HTML body, a 6 KB text alternative, 3 KB of normalized metadata, and five 8 KB raw delivery snapshots. Retaining every representation consumes 94 KB per attempt before replicas, indexes, or attachments. At ten million attempts, that example is roughly 940 GB of logical data. These are capacity-planning assumptions, not universal measurements; replace them with production distributions before setting policy.
The arithmetic points to the useful change. Keep the 51 KB rendered message, compact normalized transitions, a content hash, and a pointer to the template commit. Do not keep five nearly identical provider snapshots merely because a poller happened to observe them. Raw event bodies can have a shorter, access-controlled retention period when the normalized record preserves the fields required by policy.
Count bytes first.
| Record | Operational purpose | Retention decision |
|---|---|---|
| Template source and commit | Reproduce policy intent | Keep with release history |
| Rendered HTML and text | Prove recipient-visible content | Keep for the approved evidence period |
| Normalized event transition | Reconcile accepted, delivered, bounced, or suppressed states | Keep with the notice ledger |
| Raw provider payload | Diagnose parsing and signature disputes | Shorter period, if policy permits |
| Poll response duplicate | Temporary reconciliation input | Discard after normalization |
This trade saves storage only by giving something up. Once raw payloads expire, a later parser dispute may be investigated from hashes and normalized fields rather than replayed from the original body. Legal, security, and records owners must approve that loss; GDPR Article 5 requires personal data to be adequate, relevant, limited to what is necessary, and kept no longer than necessary, so indefinite retention is not the safe default.
Template custody is an evidence boundary
A provider-managed template can reduce deployment work, but it divides the evidence chain. The application records template key seller-restriction, while the rendered wording may depend on mutable state in another control plane. A repository-owned template makes review, testing, approval, and rollback part of the same change history as the code that selects recipients. For a compliance notice, that is the decisive advantage.
The ledger should bind four identities before any network call: a stable notice ID, the recipient subject ID, the template revision, and the hash of the rendered MIME-relevant content. Record the jurisdiction and policy decision as structured fields rather than trying to recover them from prose later. The recipient address itself can live in a more restricted data store; the audit row can reference a subject key. Suppose a seller changes locale after the restriction transaction but before a worker runs: rerendering from current profile data could produce wording that was never approved for the original decision. Capturing the selected locale, merge inputs, template commit, and rendered bytes at planning time turns that race into a reviewable record. It also makes a later resend an explicit new attempt linked to the same obligation rather than a silent mutation of the first message.
Provider-hosted templates are still defensible when non-engineering owners require controlled editing. In that model, export or snapshot the approved template version before dispatch and prohibit unversioned edits. The rule is simple: ownership may move, but the evidence cannot depend on mutable content.
For US recipients, CAN-SPAM distinguishes transactional or relationship content from commercial content by the message's primary purpose; adding promotional material can therefore change the analysis. For EU recipients, GDPR principles including purpose limitation, data minimization, accuracy, and storage limitation shape the record design. These rules do not produce one universal retention duration. Counsel and records owners must map the notice purpose and jurisdiction to a schedule.
Model delivery as a ledger, not a boolean
An email_sent flag collapses distinct facts: the marketplace decided to notify, rendering succeeded, a transport accepted the message, and a later event reported delivery. Exactly-once external delivery is not something an application can infer from one timeout. The practical target is an exactly-once business decision with idempotent submission and an auditable series of observations.
I use a unique constraint on the business obligation, such as (seller_id, notice_kind, policy_revision), and an outbox row committed in the same database transaction as the restriction. A worker claims that row, checks suppression, renders the pinned template, and submits with the notice ID as its idempotency key where the transport supports one. It appends observations; it never overwrites history.
Short states help:
-
planned: the business transaction created the obligation. -
suppressed: policy prevented submission, with a reason code and list revision. -
submitted: the transport acknowledged the request and returned an external identifier. -
delivered,bounced, orcomplained: a signed event or reconciled status established a later outcome. -
unknown: evidence remains incomplete after the reconciliation deadline.
Unknown is honest. It is also operationally actionable.
Do not guess.
The following Go sketch keeps the transport and template store generic. The transactional boundary belongs inside CreateNotice; implementations should insert the immutable notice and outbox entry atomically.
package notices
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
)
type Notice struct {
ID string
SellerID string
Kind string
PolicyRevision string
TemplateRevision string
Jurisdiction string
RenderedHTML []byte
RenderedText []byte
ContentSHA256 string
}
type Store interface {
CreateNotice(ctx context.Context, n Notice) error // Also creates the outbox row.
AppendEvent(ctx context.Context, noticeID, state, evidenceID string) error
}
type Suppressions interface {
Reason(ctx context.Context, address string) (string, bool, error)
}
type Transport interface {
Submit(ctx context.Context, idempotencyKey, address string, html, text []byte) (string, error)
}
func Dispatch(ctx context.Context, store Store, blocks Suppressions, tx Transport, n Notice, address string) error {
reason, blocked, err := blocks.Reason(ctx, address)
if err != nil {
return err // Leave the outbox claim retryable; do not guess.
}
if blocked {
return store.AppendEvent(ctx, n.ID, "suppressed", reason)
}
sum := sha256.Sum256(append(append([]byte{}, n.RenderedHTML...), n.RenderedText...))
n.ContentSHA256 = hex.EncodeToString(sum[:])
if err := store.CreateNotice(ctx, n); err != nil && !errors.Is(err, ErrAlreadyExists) {
return err
}
externalID, err := tx.Submit(ctx, n.ID, address, n.RenderedHTML, n.RenderedText)
if err != nil {
return err
}
return store.AppendEvent(ctx, n.ID, "submitted", externalID)
}
var ErrAlreadyExists = errors.New("notice already exists")
In production, render and persist before dispatch so a retry cannot pick up a newer template revision. The combined HTML-and-text hash also needs an unambiguous encoding, such as length-prefixing each part; the compact concatenation above is readable example code, not a canonical serialization format. Encryption, access logs, key rotation, and deletion workflows belong in the evidence design because the artifact contains personal data.
Polling closes gaps; events preserve chronology
Webhooks or equivalent event callbacks give low-latency transitions, while polling repairs missed or delayed observations. Process both through the same normalizer and event uniqueness constraint. A provider event ID is useful when present; otherwise derive a stable deduplication key from the external message ID, event type, provider timestamp, and documented version of the normalization rule.
Never let an older poll response regress delivered to submitted. Store observations with their source time and ingestion time, then compute the current state using an explicit transition policy. That preserves the uncomfortable facts: callbacks can arrive twice, a timeout can occur after remote acceptance, and observations can be out of order.
A reconciliation worker should select notices whose state is nonterminal and whose next-check time has elapsed, apply bounded backoff, and stop at a declared deadline. It must not resubmit merely because status lookup failed. Submission and observation are different operations.
Suppression deserves the same rigor. Use one effective view over local legal blocks, hard bounces, complaints, and transport-level suppression data; snapshot the rule revision evaluated for each attempt. A welcome message and a required restriction notice may have different legal or policy treatment, but code should consume an approved classification rather than invent one from the subject line. NIST SP 800-63B also warns that email must not be used for out-of-band authentication, which matters if a welcome message is tempted to double as an authentication factor.
Test the proof, then deploy the sender
Template tests should render every supported locale with missing optional data, escaped hostile input, long marketplace names, and both HTML and plain-text output. Snapshot tests catch wording drift, but assertions should also verify the policy revision and required fields. A golden rendering is valuable only when reviewers can trace its approval.
At the integration boundary, use a fake transport to exercise duplicate jobs, a timeout after acceptance, reordered delivery events, a newly added suppression, and a poll response that repeats an existing event. Then run a limited preproduction test against the chosen transport without real customer addresses. Verify signature validation and reject stale or malformed callbacks before they enter the ledger.
Deployment should separate template approval from worker rollout. Publish the immutable revision, obtain the required review, then enable its selection through configuration with an audit entry. Observe queue age, time from planned to submitted, unknown outcomes past the deadline, suppression counts by reason, and parser failures. Raw addresses do not belong in metric labels or logs.
Transport choice follows from this architecture rather than leading it. Amazon SES exposes account-level and configuration-set suppression concepts; SendGrid documents global and group suppressions; Mailgun documents mailing-list and bounce handling. Their boundaries and event vocabularies differ, so an adapter must preserve the marketplace's states instead of leaking one transport's taxonomy into the business ledger. Compare candidates on template export/versioning, idempotency semantics, signed event verification, regional data handling, status-query limits, and evidence export. No single score replaces those controls.
The final operating rule is retain the smallest record that can still prove the notice decision, exact rendered content, submission identity, and observed outcome. Stop keeping duplicate polls and expired diagnostic payloads. Accept that a rare late investigation will then have less transport-native detail, document that cost, and rehearse the retrieval path before an audit creates the deadline.
Further reading
- Amazon SES Developer Guide: https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- NIST SP 800-63B, Digital Identity Guidelines: https://pages.nist.gov/800-63-3/sp800-63b.html
- GDPR, Article 5 principles: https://eur-lex.europa.eu/eli/reg/2016/679/oj
- FTC CAN-SPAM Act compliance guide: https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business
- SendGrid suppression management documentation: https://www.twilio.com/docs/sendgrid/ui/sending-email/index-suppressions
- Mailgun email best practices: https://documentation.mailgun.com/docs/mailgun/email-best-practices
Top comments (0)