Three coordinated ADRs Proposed for the auth-completion sprint, requested by user: signup → magic link by email → OpenID Connect Authorization Code with PKCE, all integrated with Mailpit (already locally available as docker image) and BDD parallel testing strategy. ADR-0028 — Passwordless auth migration (sequenced): - Phase A: magic link by email (no password storage, JWT issued on consume) - Phase B: OpenID Connect Code flow with PKCE (cross-product SSO, mkcert for local HTTPS callbacks) - Phase C (separate ADR later): decommission password auth ADR-0029 — Email infra: - Mailpit (axllent/mailpit:latest) for local dev + BDD (image already pulled, 51 MB), defaults SMTP :1025 / HTTP API :8025 - pkg/email.Sender interface for provider portability - Production sender choice DEFERRED (separate ADR when volume / SLA / compliance requirements known — likely AWS SES or Postmark) ADR-0030 — BDD email parallel strategy: - Per-test recipient scoping: each scenario generates a unique address <scenario-key>-<8hex>@bdd.local - Mailpit HTTP API filters by recipient → no cross-scenario interference - pkg/bdd/mailpit/ helper package + pkg/bdd/steps/email_steps.go - Preserves the 2.85x parallel BDD speedup from PR #35 Implementation lands in subsequent PRs ; today only the design is shipped. README index updated with 3 new entries (0028/0029/0030 all Proposed).
7.1 KiB
29. Email infrastructure: Mailpit local + production deferred
Date: 2026-05-05 Status: Proposed Authors: Gabriel Radureau, AI Agent
Context and Problem Statement
ADR-0028 (passwordless auth) requires the application to send emails — magic-link tokens specifically. Email is a substrate decision : the choice of SMTP provider, the abstraction in code, and the local development experience all depend on it.
Two separate concerns :
- Local development + BDD tests : we need a local SMTP receiver that captures emails and exposes them for inspection. Real email providers (Gmail, SES, SendGrid) are unsuitable for local dev — they cost money, leak test data, and rate-limit aggressively.
- Production : the application needs to actually deliver mail to user inboxes. This decision is deferred — see "Out of scope" below.
ARCODANGE already has the Mailpit docker image pulled locally (axllent/mailpit:latest, 51 MB). Mailpit captures SMTP submissions on a port, stores them in-memory, exposes them via HTTP UI (default :8025) and an HTTP API (/api/v1/messages). It's the de-facto choice for Go projects needing local SMTP capture.
The application code needs to be provider-agnostic : a pkg/email package with a Sender interface, a Mailpit-compatible SMTP implementation, and a contract that production can swap for a real provider's adapter without changing call sites.
Decision Drivers
- Local dev and CI must work without internet — emails should never leave the docker network in tests
- Test inspection must be programmatic — BDD tests assert on email content, not just "an email was sent"
- Production decision deferred — we don't know the volume / SLA / compliance requirements yet ; over-committing now is premature
- Provider portability —
pkg/emailinterface lets us swap implementations without touching auth code - Cost — Mailpit is free, runs in a container, no API quota concerns
Considered Options
Option 1 (Chosen): Mailpit for local + tests, production via a production-grade provider TBD
- Add Mailpit to
docker-compose.yml(SMTP :1025, HTTP API :8025) pkg/emailpackage with aSenderinterface- Default implementation :
SMTPSenderconfigured against the local Mailpit in dev/CI - Tests query Mailpit's HTTP API to inspect captured messages
- Production deployment will add a separate
pkg/email/<provider>_sender.goimplementing the same interface — that decision is its own ADR
Option 2: MailHog instead of Mailpit
MailHog is the older, well-known alternative. Mailpit is its modern successor, written in Go, with a richer API and active maintenance.
- Bad — abandoned upstream (last commit 2020). Mailpit is the natural replacement.
Option 3: In-process mock email sender
Write a MockSender that captures emails in a Go slice. No SMTP at all.
- Good — fastest tests, zero infra
- Bad — doesn't validate the actual SMTP wire format, the From/To/Subject headers, the encoding of multi-byte content, or the DKIM/Reply-To setup
- Bad — doesn't double as a manual-inspection tool for the developer (no UI to look at the email)
Option 4: Send to a real but throwaway provider (Mailtrap, Mailosaur)
External services that capture-and-display emails.
- Good — production-similar paths
- Bad — costs money, requires an account, leaks test data, doesn't work offline
Decision Outcome
Chosen option : Option 1 — Mailpit for local + tests, production deferred.
Rationale :
- Mailpit is the modern, maintained successor to MailHog ; image is already on the dev machine
- The interface-first design (
pkg/email.Sender) means production swap is a future ADR, not a refactor - BDD tests have a real wire-format path to assert on (cf. ADR-0030)
- Zero monthly cost in dev/CI
Implementation Plan
pkg/email/sender.go— define theSenderinterface :type Sender interface { Send(ctx context.Context, msg Message) error } type Message struct { To string From string Subject string BodyText string BodyHTML string Headers map[string]string // for trace correlation, e.g. X-Test-Scenario-ID }pkg/email/smtp_sender.go— implementation usingnet/smtp(stdlib) configured byauth.email.smtp_host,smtp_port,smtp_username,smtp_password,smtp_use_tls. For Mailpit defaults :smtp_host=localhost smtp_port=1025 smtp_use_tls=false.pkg/email/sender_test.go— unit tests usinghttptest-style fake SMTP, plus a*_integration_test.go(build tagintegration) hitting the live Mailpit.docker-compose.yml— add themailpitservice :mailpit: image: axllent/mailpit:latest ports: - "1025:1025" # SMTP - "8025:8025" # HTTP UI / API environment: MP_MAX_MESSAGES: 5000pkg/config/config.go— add theauth.email.*config keys with defaults pointing at local Mailpit.- Documentation :
documentation/EMAIL.mdcovering local setup, message inspection via UI (http://localhost:8025), API queries.
Pros and Cons of the Options
Option 1 (Chosen — Mailpit)
- Good — already locally available, free, modern, maintained
- Good — provider-agnostic interface decouples from prod choice
- Good — full SMTP wire format = realistic test path
- Good — UI for manual inspection during dev
- Bad — requires Mailpit running (one more docker-compose service)
- Bad — production decision still pending
Option 2 (MailHog)
- Bad — unmaintained, choosing it would create immediate tech debt
Option 3 (Mock only)
- Bad — too much abstraction loss, can't catch wire-level bugs
Option 4 (Mailtrap / Mailosaur)
- Bad — cost, network dependency, account management
Consequences
- New service in
docker-compose.yml— developers rundocker compose up -donce and Mailpit is on - New
pkg/emailpackage — auth code (ADR-0028 magic link) callsSender.Send()rather than direct SMTP - New
auth.email.*config keys, new env vars (DLC_AUTH_EMAIL_SMTP_HOSTetc.) - Mailpit's HTTP API becomes part of the BDD test contract — tests use it to assert messages were sent (cf. ADR-0030)
- Production sender ADR (TBD) will be a separate decision — this ADR explicitly does NOT pick a vendor for prod
Out of scope
- Production email provider selection — separate ADR when we know volume / SLA / compliance constraints. Likely candidates: AWS SES, Postmark, SendGrid, Mailjet. Magic-link emails are transactional + low-volume — most providers handle that easily.
- DKIM/SPF/DMARC setup — production deliverability concern, not a local-dev concern
- HTML email templating — we'll start with plain-text emails ; HTML can be added with a template package (e.g.
html/template) when ARCODANGE branding requires it
Links
- Auth migration that requires this : ADR-0028
- BDD test strategy that consumes Mailpit : ADR-0030
- Mailpit homepage : https://mailpit.axllent.org/
- Mailpit API reference : https://mailpit.axllent.org/docs/api-v1/