email · side by side
Mailgun vs Postmark for a Next.js app
Both fill the email slot, so a generated repo carries one or the other, never both. Every line below is read out of the two manifests.
Short answer
Pick Mailgun if
Teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun.
Pick Postmark if
Products where the transactional mail has to arrive now: sign-in links, password resets, receipts. Also teams that send some marketing and want it kept on separate infrastructure from the mail that must not fail.
Side by side
Price, obligations, and the surface each one adds. No row is written by hand. This is manifest.yaml, rendered.
| From the manifest | Option AMailgun | Option BPostmark |
|---|---|---|
| In one line | Mailgun The veteran sending API. EU data residency and suppression lists kept server-side. | Postmark Transactional email with separate broadcast streams and 45 days of history. |
| Pricing | Mailgun Free plan: 100 emails a day and 1 custom domain. Basic starts at $15/month for 10,000 emails. Foundation is $35/month for 50,000 and Scale $90/month for 100,000. Logs are kept 1 day on Free and Basic, 5 days on Foundation and 30 days on Scale. | Postmark Free developer plan: 100 emails a month, no expiry. Paid plans start at $15/month for 10,000 emails. Every plan keeps 45 days of message history by default. Dedicated IPs (from $50/month) and longer retention are add-ons. |
| Best for | Mailgun Teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun. | Postmark Products where the transactional mail has to arrive now: sign-in links, password resets, receipts. Also teams that send some marketing and want it kept on separate infrastructure from the mail that must not fail. |
| Trade-offsVerbatim from the manifest | Mailgun
| Postmark
|
| Required companionsAdded for you, with a reason | Mailgun Nothing. It stands on its own. | Postmark Nothing. It stands on its own. |
| Recommended alongsideSuggested, never added for you | Mailgun Nothing suggested. | Postmark
|
| Env vars you will manageEvery one documented in docs/onboard.md | Mailgun 7 variables · 3 required
| Postmark 8 variables · 2 required
|
| Dependencies added | Mailgun
| Postmark
|
| MCP serversWritten into .mcp.json | Mailgun None. No extra agent tools from this one. | Postmark
|
| Footprint in your repo | Mailgun 22 files, plus 3 injections into shared stack files | Postmark 29 files, plus 3 injections into shared stack files |
What changes in your repo
The paths each battery contributes, diffed. A path in the third list is written by both, so swapping rewrites that file rather than adding one.
Only with Mailgun (2)
src/2 files
app/1 file
api/1 file
webhooks/1 file
mailgun/1 file
- route.ts
lib/1 file
email/1 file
- mailgun.ts
Only with Postmark (9)
src/3 files
app/1 file
api/1 file
webhooks/1 file
postmark/1 file
- route.ts
lib/2 files
email/2 files
- postmark.ts
- webhook-auth.ts
variants/6 files
orm-drizzle/3 files
slots/1 file
- db-schema.ts
src/2 files
db/1 file
- email-schema.ts
lib/1 file
email/1 file
- store.ts
orm-none/1 file
src/1 file
lib/1 file
email/1 file
- store.ts
orm-prisma/2 files
slots/1 file
- prisma-models.prisma
src/1 file
lib/1 file
email/1 file
- store.ts
Same path, different implementation (20)
scripts/3 files
email/3 files
- render-samples.ts
- send-test.ts
- suppressions.ts
src/13 files
lib/13 files
email/13 files
templates/7 files
- layout.tsx
- magic-link.tsx
- receipt.tsx
- reset-password.tsx
- theme.ts
- verify-email.tsx
- welcome.tsx
- address.ts
- index.ts
- outbox.ts
- retry.ts
- suppression.ts
- tags.ts
tests/2 files
unit/2 files
- email-outbox.test.ts
- email.test.ts
variants/2 files
errors-none/1 file
src/1 file
lib/1 file
email/1 file
- observability.ts
errors-sentry/1 file
src/1 file
lib/1 file
email/1 file
- observability.ts
Shared stack files Mailgun injects into
- env-required
- legal-processors
- verify-checks
Shared stack files Postmark injects into
- env-required
- legal-processors
- verify-checks
Mailgun in your .env.local
# required
EMAIL_FROM=my-app <hello@mail.example.com>
MAILGUN_API_KEY=key-replace_me
MAILGUN_DOMAIN=mail.example.com
# optional
EMAIL_OUTBOX_DIR=
MAILGUN_REGION=us
MAILGUN_WEBHOOK_SIGNING_KEY=whsk-replace_me
REPLY_TO=support@example.com
Postmark in your .env.local
# required
EMAIL_FROM=my-app <hello@mail.example.com>
POSTMARK_SERVER_TOKEN=POSTMARK_API_TEST
# optional
EMAIL_OUTBOX_DIR=
POSTMARK_BROADCAST_STREAM=broadcast
POSTMARK_MESSAGE_STREAM=outbound
POSTMARK_WEBHOOK_PASSWORD=replace_with_a_long_random_string
POSTMARK_WEBHOOK_USERNAME=postmark
REPLY_TO=support@example.com
What changes for your agents
Each battery ships rules, skills, subagents and hooks that an agent loads before it touches the code that battery owns. Picking one is also picking how your agents behave in src/lib/email/**.
Mailgun
3
Skills
1
Rules
5
Solution docs
Rules (1)
One send function, a verified domain, a reply-to, and no secrets in the body
src/lib/email/** · src/app/api/webhooks/mailgun/** · src/lib/auth/** · src/lib/billing/**
Skills (3)
/add-email-template
Add a React Email template on the shared layout, preview it, wire it into a sendEmail call, and confirm it renders as HTML and as text in a real inbox.
/diagnose-email-delivery
Work out why a Mailgun message did not arrive (region, key, suppression, authentication or placement) in the order that finds it fastest.
/test-email
Prove a send actually works: check the region and domain, send one of each template, read the Mailgun logs, inspect the suppression lists and confirm placement in a real inbox.
Subagents and hooks
None of its own. The foundation agents and guard hooks still ship.
Postmark
2
Skills
1
Rules
6
Solution docs
1
MCP servers
Rules (1)
One send path, the right message stream, and respect for inactive recipients
src/lib/email/** · src/app/api/webhooks/postmark/** · src/db/email-schema.ts · scripts/email/** · src/lib/auth/** · src/lib/billing/**
Skills (2)
/add-email-template
Add a React Email template on the shared layout, pick its message stream, wire it into a sendEmail call, and prove it lands in a real inbox.
/test-email
Prove Postmark sending works. Check the token and stream, send one of each template, read the activity feed, check suppressions, and test the basic-auth webhook with a fake bounce.
Subagents and hooks
None of its own. The foundation agents and guard hooks still ship.
What each one already knows
Solution docs land in docs/solutions/ in your repo and are published here, so you can read the failure modes before you commit.
Mailgun (5)
- Mailgun returns 401 and your API key is fine: the EU/US region trapA Mailgun domain lives in the region it was created in. Calling the wrong regional host returns 401 Unauthorized, which reads exactly like a bad API key.docs/solutions/mailgun/eu-vs-us-region-endpoints.md
- Mailgun says the message was queued and nobody receives it: sandbox domain limitsThe sandbox domain accepts every send and delivers only to five addresses you authorised. Recognise it, use it deliberately, and know when to stop.docs/solutions/mailgun/sandbox-domain-limits.md
- Suppression lists in Mailgun: stop maintaining your own bounce tableMailgun stores bounces, complaints and unsubscribes per domain and refuses to send to them. Read that list instead of building a table you have to keep in sync.docs/solutions/mailgun/suppression-lists.md
- Mailgun templates and recipient variables versus rendering HTML in your appMailgun stores Handlebars templates in its dashboard and substitutes variables at send time. Rendering in your app instead keeps email in code review: here is when each one wins.docs/solutions/mailgun/template-variables-vs-rendered-html.md
- Open tracking, click tracking and why they are off by defaultMailgun's tracking adds a pixel and rewrites every link. Both are personal data under GDPR, both cost deliverability, and neither belongs on a magic link.docs/solutions/mailgun/tracking-pixels-and-privacy.md
Postmark (6)
- Postmark bounce and spam complaint webhooks, secured without a signaturePostmark does not sign webhooks. Protect the endpoint with basic auth, act only on deactivating bounces, and make every write an upsert.docs/solutions/postmark/bounces-complaints-and-webhook-auth.md
- Postmark DKIM and custom Return-Path, and why DMARC fails without themPostmark needs two DNS records per domain. DKIM signs the mail. The pm-bounces Return-Path CNAME makes SPF align. Skip one and DMARC alignment rests on the other.docs/solutions/postmark/dkim-return-path-dns.md
- Postmark error 406: you tried to send to a recipient that has been marked as inactiveA 406 means Postmark deactivated the address after a hard bounce, complaint or manual block. Do not retry it. Surface it, and reactivate only on the owner's request.docs/solutions/postmark/inactive-recipient-406.md
- Postmark message streams, transactional vs broadcast, and why the split protects your login emailsPostmark sends transactional and broadcast mail through separate streams with separate reputations and suppression lists. Route every send on purpose, and never put marketing on the transactional stream.docs/solutions/postmark/message-streams-transactional-vs-broadcast.md
- Postmark server-side templates vs React Email rendered in your appPostmark can store templates and fill them with Mustachio. Rendering React Email in your app keeps templates in the repo, typed and reviewed. How to choose, and what each costs.docs/solutions/postmark/postmark-templates-vs-react-email.md
- Testing Postmark without emailing anyone, POSTMARK_API_TEST vs sandbox servers vs bounce-testingPostmark has three ways to send without reaching a real inbox. Each tests something different. Pick by what you need to prove.docs/solutions/postmark/testing-with-postmark-api-test-and-sandbox.md
Which one to pick
From meta.bestFor and meta.tradeoffs. If a claim is not in the manifest, it is not on this page.
Pick Mailgun when
Teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun.
And accept that(5)
- Region is chosen when the domain is created and cannot change. An EU domain must use api.eu.mailgun.net. The US host returns a 401 that reads like a bad key.
- The API is form-encoded, not JSON, which is why the SDK needs form-data as a peer. It is an older API and it shows in places.
- No React rendering on Mailgun's side. Templates are rendered to HTML in your app before sending. Mailgun's own templates are Handlebars stored in the dashboard, a different workflow this battery does not use.
- No built-in idempotency key. A retried job can send twice unless you deduplicate, so this battery attaches a deterministic Message-Id and a custom variable.
- Suppression lists live in Mailgun and are queryable through the API. No local table to maintain and one less thing to keep in sync.
Pick Postmark when
Products where the transactional mail has to arrive now: sign-in links, password resets, receipts. Also teams that send some marketing and want it kept on separate infrastructure from the mail that must not fail.
And accept that(5)
- Transactional first. Broadcast streams exist and handle unsubscribes, but there is no segmentation, no drip builder and no marketing CRM.
- Postmark does not sign webhooks. The endpoint is protected by basic auth over HTTPS, which works but puts the credential in the webhook config.
- No React rendering on Postmark's side. Templates are rendered to HTML and text in the app before sending, so the preview is exactly what ships.
- No idempotency key on the API. This battery sets a deterministic Message-ID and stores the key as metadata instead.
- New accounts go through a short approval before they can mail outside their own domain. Plan a day for it before launch.
Questions people actually ask
- Should I choose Mailgun or Postmark?
- Mailgun is best for teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun. Postmark is best for products where the transactional mail has to arrive now: sign-in links, password resets, receipts. Also teams that send some marketing and want it kept on separate infrastructure from the mail that must not fail. Both fill the email slot, so a generated repo carries one or the other, never both.
- How much do Mailgun and Postmark cost?
- Mailgun: Free plan: 100 emails a day and 1 custom domain. Basic starts at $15/month for 10,000 emails. Foundation is $35/month for 50,000 and Scale $90/month for 100,000. Logs are kept 1 day on Free and Basic, 5 days on Foundation and 30 days on Scale. Postmark: Free developer plan: 100 emails a month, no expiry. Paid plans start at $15/month for 10,000 emails. Every plan keeps 45 days of message history by default. Dedicated IPs (from $50/month) and longer retention are add-ons.
- What changes in my repo if I switch from Mailgun to Postmark?
- Mailgun writes 22 files, 7 environment variables and 5 dependencies, and installs 1 path-scoped rule, 3 skills and 5 solution docs. Postmark writes 29 files, 8 environment variables and 4 dependencies, and installs 1 path-scoped rule, 2 skills and 6 solution docs.
Decide once, then build the repo that already knows the decision.
Either way you get that choice’s rules, skills and solution docs installed, plus the guard hooks, an onboarding doc for exactly these env vars, and the Compound Engineering loop. Free and MIT.