Skip to content

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.

Mailgun compared with Postmark on pricing, fit, trade-offs, required companions, environment variables, dependencies, MCP servers and repo footprint.
From the manifestOption AMailgunOption BPostmark
In one lineMailgun

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.

PricingMailgun

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 forMailgun

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 manifestMailgun
  • 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.
Postmark
  • 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.
Required companionsAdded for you, with a reasonMailgun

Nothing. It stands on its own.

Postmark

Nothing. It stands on its own.

Recommended alongsideSuggested, never added for youMailgun

Nothing suggested.

Postmark
  • An ORM battery
Env vars you will manageEvery one documented in docs/onboard.mdMailgun

7 variables · 3 required

  • EMAIL_FROM
  • MAILGUN_API_KEY
  • MAILGUN_DOMAIN
  • EMAIL_OUTBOX_DIR
  • MAILGUN_REGION
  • MAILGUN_WEBHOOK_SIGNING_KEY
  • REPLY_TO
Postmark

8 variables · 2 required

  • EMAIL_FROM
  • POSTMARK_SERVER_TOKEN
  • EMAIL_OUTBOX_DIR
  • POSTMARK_BROADCAST_STREAM
  • POSTMARK_MESSAGE_STREAM
  • POSTMARK_WEBHOOK_PASSWORD
  • POSTMARK_WEBHOOK_USERNAME
  • REPLY_TO
Dependencies addedMailgun
  • @react-email/render ^2.1.0
  • form-data ^4.0.6
  • mailgun.js ^14.0.1
  • react-email ^6.11.0
  • server-only ^0.0.1
Postmark
  • @react-email/render ^2.1.0
  • postmark ^5.1.0
  • react-email ^6.11.0
  • server-only ^0.0.1
MCP serversWritten into .mcp.jsonMailgun

None. No extra agent tools from this one.

Postmark
  • postmark: npx -y @activecampaign/postmark-mcp@2.1.1
Footprint in your repoMailgun

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

.env.localbash10 lines
# 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

.env.localbash11 lines
# 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)

Postmark (6)

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.