Back to blog
DevelopmentTestingMailtrap AlternativeEmail Sandbox

Local Email Testing Without Mailtrap

Catch and preview emails locally with zero config. A free, offline alternative to Mailtrap, Mailhog, and mailcatcher.

SendPigeon TeamDecember 28, 20255 min read

Every developer has a horror story. You're testing a feature locally, hit send, and realize you just emailed 500 real users from your dev environment.

This guide shows you how to catch all emails locally during development, so production inboxes stay safe.

TL;DR

Quick setup:

  1. Use the SendPigeon SDK for sending emails in your app
  2. npx @sendpigeon-sdk/cli dev in one terminal
  3. SENDPIGEON_DEV=true npm run dev in another
  4. View emails at http://localhost:4100

No real emails sent. Ever.


The Problem

During development, you need to test email flows:

  • Password resets
  • Welcome emails
  • Order confirmations
  • Notifications

But sending real emails from localhost is risky:

  • Accidental sends - One wrong database seed and you email everyone
  • Rate limits - Burning through your quota on test emails
  • Deliverability - Test emails can hurt your sender reputation
  • Slow feedback - Waiting for emails to arrive, checking spam folders

Common Solutions

Cloud Sandboxes (Mailtrap, etc.)

Cloud-hosted email sandboxes with web UIs for inspecting caught emails.

Trade-offs:

  • Requires internet connection
  • Account signup and SMTP credential configuration
  • Free tiers have email limits (e.g., 50/month)
  • Paid plans for higher volumes

Self-Hosted (Mailhog, mailcatcher)

Run a local SMTP server that catches all emails.

Trade-offs:

  • Requires Docker or binary installation
  • SMTP configuration in your app (change host/port)
  • Separate config from production code
  • Works offline once set up

SDK-Integrated (SendPigeon Dev)

A different approach: the SDK routes to a local server automatically.

Benefits:

  • One command to start: npx @sendpigeon-sdk/cli dev
  • No code changes - just set an environment variable
  • Same code path as production
  • Works offline, no account needed

The Solution: Local Email Catching

Instead of sending emails to real inboxes, catch them locally. The email never leaves your machine - it's captured and displayed in a web UI.

Your App → SendPigeon SDK → localhost:4100 → Web UI
                                ↓
                         (no real email sent)

Setup

1. Start the Dev Server

In one terminal:

npx @sendpigeon-sdk/cli dev

Output:

SendPigeon Dev Server

  API: http://localhost:4100/v1/emails
  UI:  http://localhost:4100

2. Run Your App in Dev Mode

In another terminal, set SENDPIGEON_DEV=true:

SENDPIGEON_DEV=true npm run dev

The SDK automatically routes to localhost:4100 instead of production.

3. Send an Email

Trigger any email in your app - signup, password reset, etc.

4. View in the UI

Open http://localhost:4100. Your email appears instantly:

  • See HTML and plain text versions
  • Check subject, from, to addresses
  • No waiting for delivery
  • No checking spam folders

How It Works

When SENDPIGEON_DEV=true is set, the SDK changes its behavior:

// Normal mode
const client = new SendPigeon("sp_live_xxx");
// → sends to api.sendpigeon.dev

// Dev mode (SENDPIGEON_DEV=true)
const client = new SendPigeon("sp_live_xxx");
// → sends to localhost:4100

You'll see a log confirming dev mode:

[SendPigeon] Dev mode → http://localhost:4100

Your code stays the same. No conditional logic needed.


Package.json Scripts

For teams, add scripts to your package.json:

{
  "scripts": {
    "dev": "next dev",
    "dev:email": "sendpigeon dev",
    "dev:full": "concurrently \"npm run dev:email\" \"SENDPIGEON_DEV=true npm run dev\""
  },
  "devDependencies": {
    "@sendpigeon-sdk/cli": "^1.0.0",
    "concurrently": "^8.0.0"
  }
}

Now npm run dev:full starts everything at once.


CI/CD Considerations

In CI, you might want to:

  1. Skip emails entirely - Set a flag to disable email sending in tests
  2. Use the dev server - Start it in CI, run tests, verify emails were "sent"
  3. Mock at the SDK level - For unit tests, mock the SendPigeon client

Example test setup:

// vitest.setup.ts
import { vi } from "vitest";

vi.mock("sendpigeon", () => ({
  SendPigeon: vi.fn().mockImplementation(() => ({
    send: vi.fn().mockResolvedValue({ data: { id: "test_123" }, error: null }),
  })),
}));

Best Practices

Use Environment Variables

# .env.local (development)
SENDPIGEON_DEV=true
SENDPIGEON_API_KEY=sp_test_xxx

# .env.production (never set SENDPIGEON_DEV)
SENDPIGEON_API_KEY=sp_live_xxx

Separate Test and Live Keys

Even in dev mode, use a test API key (sp_test_xxx). Test keys disconnect from AWS SES entirely - no real emails can be sent, even if SENDPIGEON_DEV accidentally gets unset.

Clear Emails Regularly

The dev server stores up to 100 emails in memory. Click "Clear All" when testing fresh flows.


Troubleshooting

Emails not appearing?

  1. Check SENDPIGEON_DEV=true is set
  2. Look for [SendPigeon] Dev mode in your app's logs
  3. Verify dev server is running on port 4100

Port already in use?

npx @sendpigeon-sdk/cli dev --port 4200

Then set baseUrl explicitly:

const client = new SendPigeon("sp_test_xxx", {
  baseUrl: "http://localhost:4200",
});

Next Steps


Comparison

FeatureSendPigeon DevCloud SandboxSelf-Hosted
Works offlineYesNoYes
Setup1 commandAccount + SMTPDocker + SMTP
Code changesEnv var onlySMTP configSMTP config
Same as productionYesNoNo

Each approach has its place. Cloud sandboxes work well for teams who want shared visibility. Self-hosted is great if you're already using Docker. SDK-integrated is ideal when you want zero friction between dev and production code.


Summary

Local email testing is essential for safe development:

Without Dev ServerWith Dev Server
Risk of accidental sendsEmails caught locally
Burns API quotaZero API calls
Slow feedback loopInstant preview
Check spam foldersAll in one UI

One command to start, one env var to enable. No more accidentally emailing real users from localhost.

npx @sendpigeon-sdk/cli dev