Local-First Email Development
A case for building email the way you build software: local dev, version-controlled sequences, one tool from localhost to production. No more duct-taping five services together.
Local-first email development means building and testing email locally during development, then shipping to production with the same code and same SDK — no config changes, no separate tools.
Most email setups in production look different:
- Resend for transactional ($20/mo)
- Customer.io for sequences ($100/mo)
- Mailchimp for broadcasts ($13/mo)
- Mailpit for local testing (free, but separate)
- Postmark inbound for receiving emails ($15/mo)
Five services. Five dashboards. Five sets of credentials. Five things to keep in sync.
The Fragmentation Problem
Sending a welcome email, running an onboarding drip, and receiving a customer reply often means three different vendors with three different APIs. Not because the tools are bad — each one is good at its slice:
- Transactional APIs optimized for delivery speed and deliverability
- Marketing platforms optimized for visual editors and audience management
- Automation tools optimized for journey builders and behavioral triggers
- Local testing tools optimized for catching SMTP during development
- Inbound parsers optimized for converting emails to webhooks
Each does its job well. But the integration overhead adds up — especially for small teams where one engineer owns all of email.
What Local-First Means
Local-first development is a principle: your development environment should mirror production as closely as possible, without depending on external services.
For email, this means:
- Local capture — All emails sent during development are caught locally. No real sends. No API calls. No risk.
- Same code path — The code that sends a welcome email locally is identical to what runs in production. No conditional logic, no environment switches.
- Instant feedback — Send an email, see it in your browser. Sub-second. No checking inboxes, no waiting for delivery.
- Offline capable — Works without internet. Works on planes. Works when AWS is down.
This isn't new. Databases have SQLite for local dev. Message queues have in-memory brokers. But email has been stuck in the "hit the production API and hope for the best" era.
The Five-Service Problem
Here's what a SaaS product's email needs look like after 12 months:
| Need | Typical solution | When it appears |
|---|---|---|
| Password resets, receipts | Transactional API (Resend, Postmark) | Day 1 |
| Local testing | Mailpit, Mailhog | Day 1 |
| Onboarding drip | Automation tool (Customer.io, Loops) | Month 3 |
| Product updates | Marketing tool (Mailchimp, ConvertKit) | Month 6 |
| Reply-by-email, support | Inbound parsing (Postmark, Mailgun) | Month 9 |
By month 9, you're maintaining five integrations. Each has its own:
- Authentication mechanism
- Template system
- Contact/user model
- Webhook format
- Dashboard
- Billing
And none of them talk to each other. Your onboarding sequence in Customer.io doesn't know about the bounce events in Resend. Your Mailchimp contacts don't sync with your Postmark suppressions.
One Tool, From Localhost to Production
What if you could:
# Development
npx @sendpigeon-sdk/cli dev
// Same code, everywhere
const pigeon = new SendPigeon("sp_live_xxx");
// Transactional
await pigeon.emails.send({ from, to, subject, html });
// Sequences
await pigeon.sequences.create({ name: "Onboarding", triggerType: "CONTACT_CREATED" });
// Broadcasts
await pigeon.broadcasts.send({ contactFilter: { tag: "active" }, templateId: "..." });
// Inbound (just set up MX records, receive webhooks)
// POST /api/inbound → { event: "email.received", data: { from, to, subject, text } }
In dev mode, everything is caught locally. In production, everything goes through the same API. One SDK. One set of types. One dashboard.
No Resend + Customer.io + Mailchimp + Mailpit + Postmark inbound. Just one.
What Changes
For solo developers and small teams
You stop juggling credentials and dashboards. Your email stack becomes as simple as your database — one connection string, one tool, one bill.
For growing teams
Onboarding sequences, marketing broadcasts, and transactional emails share the same contact model. A bounce on a transactional email automatically updates the contact's suppression status across all channels. No sync scripts. No webhook relay.
For your codebase
# Before
.env
├── RESEND_API_KEY=re_xxx
├── CUSTOMERIO_API_KEY=cio_xxx
├── MAILCHIMP_API_KEY=mc_xxx
├── POSTMARK_SERVER_TOKEN=pm_xxx
└── MAILPIT_URL=localhost:8025
# After
.env
├── SENDPIGEON_API_KEY=sp_live_xxx
└── SENDPIGEON_DEV=true # only in dev
The Mailhog Lineage
If you've been using Mailhog, Mailpit, or Mailcatcher for local email testing, you already get this. Catching emails locally instead of sending them to real inboxes is obvious once you've done it.
The logical next step is: what if the production tool worked the same way?
Set an environment variable and your production email SDK captures locally. Remove it and it sends for real. Same code. Same templates. Same types.
That's what local-first email development means.
The Bet
We're betting that developers want fewer tools, not more. That the right abstraction for email isn't five specialized services — it's one platform that handles the full lifecycle, from localhost:4100 to production inboxes.
This is what we're building with SendPigeon. One command to start. One SDK to learn. One tool from prototype to production.
npx @sendpigeon-sdk/cli dev
Next Steps
- Local email testing without Docker — set up in 30 seconds
- Inbound email parsing — receive emails as webhooks
- Drip email API — build sequences in code
- 7 best Mailhog alternatives — compare local testing tools
- Get started with SendPigeon — free, no credit card