For a transactional email service sending game welcome emails, keep templates in the application repository and put bounce suppression behind a small delivery adapter. That arrangement keeps custom-domain and EU/US compliance work visible instead of burying it in transport configuration. The deciding constraint is template ownership: if switching a transport also means rebuilding copy, localization, and tests in a provider dashboard, the transport has quietly become part of the product.
TL;DR: Treat rendering, delivery, and recipient eligibility as three separate decisions. Render a complete message from versioned data, check a suppression store, then hand the result to a transport. Accept bounce events through an authenticated, idempotent ingestion path and suppress permanent failures before the next send. Resend, Postmark, and Amazon SES expose different template, event, and operational boundaries; regional and contractual requirements remain the sender's work.
Which transactional email service keeps welcome emails compliant?
A player welcome message looks harmless until it contains a display name, locale, deep link, parental-consent wording, and live-operations copy. Put that template only in a delivery service's editor and review becomes split: code changes follow one process while customer-facing behavior follows another. Rollback may require coordinating two histories.
That split matters.
Application-owned templates need a rendering toolchain, preview fixtures, HTML escaping, plain-text output, and tests. That is real work for a small team. I would still pay that cost when game state determines the message, because the compiler and pull request become the audit trail. For copy edited frequently by non-engineers, provider-managed templates can be a deliberate choice instead.
The initial assumption is tempting: store one template in every provider and pass provider-specific variables from each adapter. The first send is short. Migration testing exposes the mistake because fixtures must match several template engines. The chosen experiment renders once in application code and keeps adapters boring. This is a clear trade-off: more local rendering work in exchange for one review history and repeatable locale fixtures.
Keep it dull.
A narrow TypeScript boundary
type WelcomeInput = {
playerId: string;
email: string;
displayName: string;
locale: "en-US" | "de-DE";
verifyUrl: string;
};
type RenderedEmail = {
from: string;
to: string;
subject: string;
html: string;
text: string;
headers: Record<string, string>;
};
interface Suppressions {
isBlocked(email: string): Promise<boolean>;
block(email: string, reason: "hard-bounce" | "complaint"): Promise<void>;
}
interface EmailTransport {
send(message: RenderedEmail): Promise<{ providerMessageId: string }>;
}
async function sendPlayerWelcome(
input: WelcomeInput,
suppressions: Suppressions,
transport: EmailTransport,
): Promise<"sent" | "suppressed"> {
if (await suppressions.isBlocked(input.email)) return "suppressed";
const messageId = crypto.randomUUID();
await transport.send(renderWelcome(input, messageId));
return "sent";
}
renderWelcome should escape player-controlled fields, produce HTML and plain text, and use a fixed fixture for every supported locale. Never log the verification URL; it can carry a token. OWASP's forgot-password guidance targets reset flows, but its token properties transfer cleanly to verification links: use a cryptographically secure source, make tokens single-use, expire them, store them securely, and avoid account enumeration.
There is no provider template ID or generic substitution bag in the interface. Deliberately. An adapter may map the message to a native API, but game code does not inherit that API's template model.
Bounce handling is a state transition
A successful API response means the transport accepted the message, not that a mailbox accepted it. Delivery events arrive later and can be duplicated or reordered. Ingestion needs authentication according to the sender's documented mechanism, durable deduplication, and a mapping from external to internal message IDs.
Do little on the request path: verify the event, persist its immutable envelope, acknowledge it, and process it asynchronously. Permanent failures and complaints should block future sends to that address. Transient failures should feed retry policy and monitoring rather than immediately becoming permanent suppression. Amazon SES documents bounces and complaints as reputation signals; its mailbox simulator supports testing without harming reputation.
The suppression record needs a normalized address key, reason, observed time, source event ID, and policy version. Keep the original event separately with access controls and retention rules. An address is personal data, so EU/US deployment is not solved by selecting a region. Document purpose, retention, processors, transfer terms, deletion behavior, and who can inspect payloads. Legal review sets those rules; architecture enforces them.
Gaming adds an awkward case. Several child or family profiles may reuse a guardian's address. Suppressing by player ID sends again to a known-bad mailbox; suppressing by address affects every linked profile. Model both relationships, audit support actions, and never auto-unsuppress merely because a player retries a form.
Three services, three ownership boundaries
All three candidates can sit behind the transport interface, but their boundaries differ. Resend exposes sending and hosted templates; choosing those templates places versions in its control plane, while rendered HTML keeps ownership in the application. Postmark documents hosted templates, aliases, layouts, and validation, which can help an editorial workflow but makes those constructs migration work. Amazon SES supports formatted, raw, and templated email plus account-level suppression and mailbox-simulator workflows; SES templates and AWS event plumbing enlarge the AWS-specific surface.
Those are trade-offs, not rankings.
| Decision | Evidence to collect | Failure to force |
|---|---|---|
| Template ownership | Export, versioning, previews, locale fixtures | Roll back copy without code |
| Domain control | DNS records, alignment, verification lifecycle | Rotate a signing key |
| Bounce path | Signed-event docs, retries, event identifiers | Duplicate hard bounces |
| Data governance | DPA, subprocessors, regions, deletion | Trace deleted-player data |
| Operations | Quotas, sandbox rules, alert export | Sudden bounce-rate increase |
Do not accept a checkbox answer for custom domains. Verify SPF and DKIM behavior, DMARC alignment, return-path handling, and DNS rotation. Send to controlled valid, invalid, and delayed recipients. Provider simulators help where documented, but an end-to-end test on a dedicated subdomain catches DNS and correlation mistakes.
Compliance deserves the same discipline. Ask where content and event metadata are processed, which region choices cover every dependency, what terms govern transfers, and how deletion requests propagate into logs, suppression data, and backups. A US entity and an EU region are different answers.
What should be measured before copying this design?
Measure acceptance latency separately from delivery-event latency. Record p50, p95, and p99, then count accepted, delivered, transient, permanent-bounce, complaint, and suppressed outcomes using stable internal IDs. Alert on rates and changes rather than totals, because launch-day welcome volume should not resemble an incident.
Measure template work too: preview time across locales, escaping and missing-field coverage, rollback time, and provider-specific concepts exposed above the adapter. That last count is an honest lock-in signal. Zero is unrealistic once event authentication and operations enter the picture, but the coupling should be explicit.
Cost belongs in the evaluation, just not in the headline. Include preview maintenance, event ingestion, deliverability monitoring, audit requests, and migration work alongside message charges. For a solo builder, time spent reconciling an opaque bounce queue can dominate a small rate-card difference. Choose the ownership model first, prove the failure paths, and only then compare the variable bill at expected volume.
References
- https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- https://docs.aws.amazon.com/ses/latest/dg/send-email-concepts-deliverability.html
- https://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list.html
- https://resend.com/docs/dashboard/emails/templates
- https://postmarkapp.com/developer/user-guide/templates/templates-overview
- https://postmarkapp.com/developer/webhooks/webhooks-overview
- https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- https://www.rfc-editor.org/rfc/rfc7489
Top comments (0)