deadLetter v1.2.0
Local-first message observability

Where messages
go to rest.

A local-first cockpit for SMTP, Kafka, Redis, and webhooks — so your next test email lands in a UI, not a customer inbox. One binary. One port. Zero accidental deliveries.

30s
to first capture
0
surprise deliveries
1 bin
Go · Docker · brew
MIT
self-hosted · free
localhost:8025/
live
LIVE · 6693 CAPTURED

messages go to rest

Email
6693 captured
listening :1025
Kafka
:9092 broker
offline
Redis
8.6.2 version
connected
Webhooks NEW
/webhooks/*
listening
deadLetter v1.2.0 · replay-ready · zero deliveries
SMTP :1025 HTTP :8025 SSE live Webhooks
SMTP sink HTML sandbox Raw .eml on disk Replay on demand Retention windows Kafka browser Schema Registry Redis Pub/Sub Webhooks SSE stream CLI · deadletterctl
SMTP sink HTML sandbox Raw .eml on disk Replay on demand Retention windows Kafka browser Schema Registry Redis Pub/Sub Webhooks SSE stream CLI · deadletterctl
00 / the ui

A cockpit you'll actually want to open.

No terminal required. deadLetter ships a static Next.js UI that lives at localhost:8025 — inbox, Kafka workspace, Redis tools, webhooks, settings. Live updates over SSE. Dark mode by default.

everything below mirrors the real UI
localhost:8025/
live
deadLetter v1.2.0
LIVE · 6693 CAPTURED

where messages go to rest

Capture, inspect, and debug SMTP · Kafka · Redis · Webhooks — one place, one shell, zero noise.

Email Inbox
SMTP sink for local development
6693
captured
2.0 MB
storage
:1025
smtp
:8025
http
listening
Kafka
Topics, consumers, messages
0
topics
0
consumers
localhost:9092
broker
registry
offline
Redis
Pub/Sub channels and keys
8.6.2
version
0
keys
localhost:6379
server
0
db
connected
Webhooks NEW
Capture, inspect, replay HTTP
0
captured
off
forward
/webhooks/*
endpoint
hmac
verify
listening
inbox topics channels hooks keys health
deadLetter v1.2.0
localhost:8025/email
live
SMTP listening · :1025 REPLAY Enabled TODAY 24 UPTIME 4m 11s EVENTS connected New email from Acme <noreply@acme.test>
Inbox 24
All · Newest
  • AN
    Welcome to Acme
    Acme <noreply·less than a minute ago
  • MY
    Build report
    myfpus·1 minute ago
  • PX
    Build report
    pxnfgx·1 minute ago
  • YN
    Automated email
    ynyaky·1 minute ago
  • KD
    Random notification
    kdktxy·1 minute ago
  • HS
    Hello there 👋
    hsbvzp·1 minute ago
  • IA
    Automated email
    iaheyf·1 minute ago
  • YA
    Deadletter ping
    yahvrq·1 minute ago

Welcome to Acme

Acme <noreply@acme.test> you@deadletter.local
less than a minute ago
Welcome aboard

Hey there — your account is ready. Click the button below to get started.

Get started

If you didn't sign up, ignore this message.

/kafka · tail orders
p0140619:23:47{"orderId":"A7F31","total":82.10}
p2140719:23:48{"orderId":"A7F32","total":14.00}
p189119:23:49{"orderId":"A7F33","total":221.50}
p2140819:23:49{"orderId":"A7F34","total":9.99}
p0140719:23:50{"orderId":"A7F35","total":45.00}
p189219:23:51{"orderId":"A7F36","total":9.00}
p2140919:23:51{"orderId":"A7F37","total":60.10}
3 partitions·lag 0·group: inventory-svc
/redis · subscribe notif:*
1.4K
msg/s
12
channels
2.1 MB
mem
  • notif:user:19profile.updated
  • notif:order:A7F31shipped
  • notif:user:42login
  • notif:cart:8abandoned
/webhooks · deliveries
  • dev-bot https://hooks.local/dev
    email.captured · 47ms · HMAC ✓
    200
  • slack https://hooks.slack.com/…
    email.duplicate · 112ms · HMAC ✓
    202
  • analytics https://analytics.local/in
    retry 2/5 · exp backoff · 1.3s
    429
  • qa-inbox https://qa.local/capture
    kafka.message · 23ms · HMAC ✓
    200
  • legacy-bus https://old.local/recv
    disabled · 5 consecutive 5xx
    500
static export zero runtime deps SSE live updates dark / light ⌘K everywhere keyboard first
01 / features

Everything your messages touch,
in one cockpit.

Watch SMTP, tail Kafka, poke at Redis, fire webhooks — from one UI, one API, one binary. Built for the two places production doesn't want you: your laptop and CI.

SMTP capture

:1025 · raw .eml + JSON

Core

Point your app at localhost:1025. deadLetter accepts the message, persists the raw envelope, extracts headers and MIME parts, and rebuilds its index on boot so captures survive restarts.

inbox · 3 new ⌘K
  • Welcome to deadLetter app@local 19:23
  • [CI] build #1247 passed ci@runner 19:22
  • Possible duplicate · password reset auth@svc 19:21

HTML sandbox

Render sanitized HTML previews safely while keeping the original raw source untouched on disk.

raw rendered text

Webhooks

Fan out captured events to HTTP endpoints with retries, HMAC signatures, and delivery logs.

POST /api/webhooks

Kafka observability

Brokers, topics, partitions, consumer groups, offsets. Tail a partition, reset a group, manage topics, and deserialize Confluent wire-format with Schema Registry integration.

brokers topics tail offsets schemas

Redis tools

Pub/Sub monitoring, live channel subscriptions over SSE, server info, key discovery, and DB usage — all from the same UI.

pub/sub channels keys info

Replay, opt-in

Re-send a captured message to a real SMTP target only when you explicitly enable it. Zero surprises.

Retention

Keep messages for a configurable window. Scheduled or manual cleanup. Disk never surprises you.

Live SSE

The UI streams backend events — email, replay, retention, Kafka, webhooks — so what you see is what just happened.

deadletterctl

List, show, replay, and export captured emails from a bundled CLI. Friendly to automation, CI, and muscle memory.

$ deadletterctl replay 01HZX… --to qa@internal

Duplicate detection

deadLetter flags likely repeats based on sender + subject within a short window, so misbehaving workers stop hiding.

08 / comparison

The only tool that covers
all four surfaces.

Everyone else nails one protocol. deadletter collapses four into a single local binary — SMTP, Kafka, Redis, webhooks — with no SaaS, no signup, no telemetry.

Tool Email Kafka Redis Webhooks Price
deadletter Free · OSS
Mailpit Free
MailHog Free · unmaintained
MailCatcher Free
Mailtrap Free tier · paid
webhook.site Free · paid
Smee.io Free
Hookdeck Free tier · paid
ngrok inspect Free · paid
Kafka UI Free
Redpanda Console Free · Enterprise
AKHQ Free
Conduktor Free tier · paid
RedisInsight Free
Redis Commander Free

Scope: tools that replace at least one of deadletter's four surfaces. General-purpose tools (Postman, Wireshark, log aggregators) excluded.

02 / clean api

Honest JSON. Honest streams.

Every view in the UI is backed by a documented HTTP endpoint and a live SSE topic. Nothing is hidden, nothing is proprietary.

GET /api/emails/:id 200 · application/json
// Full metadata + parsed MIME for one capture
{
  "id":         "01HZX5A6QH7B0Q2F3TJ6PE8M7N",
  "from":       "app@local",
  "to":         ["alice@example.com"],
  "subject":    "Welcome to deadLetter",
  "receivedAt": "2026-04-17T19:23:11Z",
  "size":       4123,
  "parts":      ["text/plain", "text/html"],
  "duplicate":  false,
  "replayed":   false,
  "links": {
    "raw":      "/api/emails/01HZX…/raw",
    "html":     "/api/emails/01HZX…/html",
    "text":     "/api/emails/01HZX…/text"
  }
}
STREAM /events/stream text/event-stream
// Live backend events
event: email.captured
data: {"id":"01HZX…","from":"app@local","size":4123}

event: email.duplicate
data: {"id":"01HZX…","within":"30s"}

event: webhook.delivered
data: {"endpoint":"dev-bot","status":200,"ms":47}

event: kafka.message
data: {"topic":"orders","partition":2,"offset":1407}

event: retention.swept
data: {"removed":14,"keep":"24h"}

03 / architecture

One binary.
One UI. Many sources.

A Go backend with a static Next.js UI baked in. Ingest SMTP, tail Kafka, watch Redis — everything funnels through the same live SSE bus, so the UI never lies about what the backend just saw.

sources
your app
SMTP → :1025
kafka broker
SASL / PLAINTEXT
redis
pub/sub · info · keys
deadletter core
deadLetter
Go
  • SMTP sink + .eml store
  • MIME parser + sanitizer
  • in-memory index (rebuild on boot)
  • kafka + redis clients
  • retention scheduler
  • webhook dispatcher
  • SSE event bus
consumers
web UI
static next.js · served at /
HTTP JSON API
curl · scripts · bots
webhooks
signed · retried · logged
SSE stream
/events/stream
04 / quickstart

Pick a path.
Ship in a minute.

From a one-line Docker pull to a full Kafka + Redis stack — plus a Homebrew formula landing soon. Copy, paste, done.

prerequisites Docker 20+ port 1025 free port 8025 free Kafka / Redis (optional)
fastest · two commands · ~45s
$ docker pull peixotomdb/deadletter:latest

$ docker run -d \
    --name deadletter \
    -p 1025:1025 -p 8025:8025 \
    -v "$PWD/data:/data" \
    peixotomdb/deadletter:latest

# UI & API  → http://localhost:8025
# SMTP sink → localhost:1025
2 Verify it's alive
$ curl -s localhost:8025/api/health
{"status":"ok","version":"1.2.0","smtp":"1025","uptime":"3s"}
full stack · point at broker + redis
$ docker run -d \
    --name deadletter \
    -p 1025:1025 -p 8025:8025 \
    -v "$PWD/data:/data" \
    -e KAFKA_BROKERS=host.docker.internal:9092 \
    -e SCHEMA_REGISTRY_URL=http://host.docker.internal:8081 \
    -e REDIS_ADDR=host.docker.internal:6379 \
    peixotomdb/deadletter:latest

# Kafka topics, consumers & schemas show up in /kafka
# Redis keys & pub/sub channels show up in /redis
env var default notes
KAFKA_BROKERScomma-sep list · SASL supported
SCHEMA_REGISTRY_URLoptional · enables Avro decoding
REDIS_ADDRsingle node · user:pass@host ok
REPLAY_ENABLEDfalseopt-in SMTP re-send
RETENTION24hGo duration string
coming soon · native macOS binary
# formula in review — expected in the next release
$ brew install peixotomdb/tap/deadletter

$ deadletter
▸ SMTP sink   listening on :1025
▸ HTTP API    listening on :8025
▸ data dir    ~/Library/Application Support/deadletter

$ deadletter --help  # CLI mirrors the REST API
Get pinged when the formula lands
Open issue →
go 1.22 · pnpm 9 · make
$ git clone https://github.com/peixotomdb/deadletter.git
$ cd deadletter-ui && pnpm install && pnpm build
$ cd .. && go build -o deadletter ./cmd/deadletter
$ ./deadletter

# or live UI + backend dev
$ make dev
Something broke?
Error: bind: address already in use Another tool (MailHog? Mailpit?) holds 1025/8025. Map different host ports: -p 2525:1025 -p 18025:8025.
connection refused · host.docker.internal Linux hosts need --add-host=host.docker.internal:host-gateway. Docker Desktop resolves it automatically.
No events in UI Check curl localhost:8025/events/stream returns. If yes, your app is silent — verify SMTP host, not firewall.
05 / ports

Default ports

overridable via env
  • :1025
    SMTP sink
    point your app here
  • :8025
    HTTP API + UI
    JSON endpoints, SSE, static UI
  • :9092
    Kafka (compose)
    optional — only in full stack
  • :8081
    Schema Registry
    optional — only in full stack
  • :6379
    Redis (compose)
    optional — only in full stack
06 / ui routes

UI surfaces

  • /
    Dashboard
    counts, live feed, recent captures
  • /inbox
    Inbox
    searchable list · raw · rendered · MIME
  • /kafka
    Kafka workspace
    brokers · topics · tail · consumer groups
  • /redis
    Redis workspace
    pub/sub · channels · keys · info
  • /webhooks
    Webhooks
    endpoints · signatures · delivery logs
  • /settings
    Settings
    retention · replay · theme · sound
07 / safety

Safe by default.
Loud when it isn't.

deadLetter is built for local and CI environments where an accidental delivery can mean a real user gets a test email. Every destructive action is opt-in, logged, and visible in the UI.

Replay is opt-in

Until you flip REPLAY_ENABLED=true and point it at a target, replay returns an error.

Raw is untouched

The .eml on disk is byte-exact. Previews sanitize separately.

Sandboxed HTML

Rendered HTML is served in a sandboxed frame with no network egress by default.

Signed webhooks

Outbound webhooks carry an HMAC-SHA256 signature and retry with exponential backoff. Logs are keepable.

What deadLetter is not

Not a production SMTP relay. Not a deliverability service. Not a hardened multi-tenant mailbox. It is a local-first observability cockpit — deliberately small, deliberately loud about what it sees.

Ready when you are

Stop sending test email
into the void.

One docker run — and every SMTP, Kafka, Redis, and webhook event your dev env emits lands in a UI you can actually reason about.

localhost:8025/
live
LIVE · 6693 CAPTURED

messages go to rest

Email
6693
captured
listening
Kafka
:9092
broker
offline
Redis
8.6.2
version
connected
Webhooks NEW
/webhooks/*
endpoint
listening