A Transactional Email Provider Checklist That Starts With the App
A practical way to choose Resend, Postmark, SendGrid, Mailgun, SES, SMTP, or a mixed provider setup without turning every email into vendor glue.
Most provider comparisons start in the wrong place. They rank dashboards, templates, prices, and brand names before asking what the app actually needs to send.
Start with the product email, not the vendor. A login code, invoice receipt, export-ready notification, and customer-hosted sender domain do not have the same risk profile. One provider can be perfect for one and awkward for another.
Sort by message shape
Write down the fields each class of email needs before choosing a provider. Include attachments, custom headers, reply-to addresses, metadata, tags, and tenant-specific sender domains. Those details are where provider APIs stop feeling interchangeable.
| Likely requirement | Provider question | |
|---|---|---|
| Login code | Plain text or simple HTML | Can the backup send it from the same trusted domain? |
| Receipt | Metadata, headers, maybe PDF attachments | Does the fallback support every field? |
| Product digest | Tags, unsubscribe behavior, template data | Does this belong in a campaign tool instead? |
| Audit notice | Stable IDs and traceability | Can logs prove which route sent it? |
Pick a default and a boring backup
The default provider should match your normal product path. The backup provider should match the smallest safe version of the same message. If the backup cannot carry the fields your support or billing team relies on, it is not a backup. It is a different email.
- Confirm the sender domain works on both routes.
- Check field support before enabling a fallback.
- Log the adapter name and provider response ID.
- Keep retry count low for emails a user might notice twice.
Keep provider details at the boundary
Provider SDKs are fine inside an adapter. They get painful when their payload shape leaks into product code. A clean app boundary lets you replace, add, or split providers without hunting through every password reset and invoice path.
const email = createEmailClient({
adapters: [resendAdapter, postmarkAdapter, smtpAdapter],
defaultAdapter: "resend",
fallback: ["postmark"],
});