Skip to content
BoringStack
GitHub

Email in development

2 min read

Local email

Iterate on transactional email templates and signup flows locally without sending real mail. BoringStack includes Mailpit: SMTP on :1025, web UI on :8025.

Mailpit

SMTP Catcher

1025

SMTP Port

8025

Web Port

Test verify-email, password-reset, welcome, and notification flows locally without sending through Cloudflare Email Service, Resend, or SendGrid.

  1. Start the dev stack with the Mailpit overlay:

    Terminal window
    WITH_MAILPIT=1 ./dev.sh up -d

    Adds two host ports: 1025 (SMTP) and 8025 (web UI).

  2. Point the api at the catcher. In infra/compose/compose/api.dev.env (create if it doesn’t exist):

    Terminal window
    EMAIL_PROVIDER=smtp
    SMTP_HOST=mailpit
    SMTP_PORT=1025
    # SMTP_USER + SMTP_PASS are optional. Mailpit accepts any.
  3. Restart the api so it picks up the new env:

    Terminal window
    ./dev.sh restart api-dev
  4. Trigger an email: sign up, hit forgot-password, anything that emits transactional mail.

  5. Open the web UI:

    Terminal window
    open http://localhost:8025

    Full HTML, plain-text alt, headers, raw source.

  • Designing a new email template. Edit the Handlebars source under apps/api/src/templates/email/, trigger a flow, refresh Mailpit.
  • Debugging delivery locally. Mailpit captures the full SMTP envelope, so you can verify From / Reply-To / List-Unsubscribe before going near a real provider.
  • E2E tests that exercise email. Point the api at Mailpit and assert against the API surface (http://localhost:8025/api/v1/messages). No mail leaves the host.
  • Validating deliverability or spam scores. Mailpit doesn’t send; you need a real provider in staging.
  • Load-testing the email pipeline. Mailpit defaults cap at 5,000 retained messages.

The same EMAIL_PROVIDER=smtp provider works against any RFC-5321 server (Postfix sidecar, SES via SMTP, Postmark). Set SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS in api.prod.env.