MailTrooper
Shared email infrastructure for a family of small products — bulk campaigns and transactional sends, contacts and segments, open/click tracking and webhooks. Being rewritten in TypeScript.
- 286,000+
- mail events processed
- Campaigns + transactional
- one shared pipeline
- Full loop
- opens, clicks, bounces, unsubscribes
The problem
A studio running several small products doesn't want to wire up email — sending, tracking, bounces, unsubscribes — separately in each one. MailTrooper is the shared email backend they all send through: both marketing campaigns and the transactional email that products actually depend on.
What it does
It handles the full lifecycle of both kinds of email: bulk campaigns and one-off transactional sends both funnel through the same worker. It manages contacts, segments and templates, tracks opens and clicks, handles subscribe and unsubscribe, and processes delivery events — bounces and complaints — coming back from the email provider, so a sender's list stays healthy automatically.
A less-obvious feature: 'sources' can auto-pull content from feeds — RSS, sitemaps, social channels — to seed newsletter content. Accounts can register their own outbound webhooks to react to email events, and billing runs on a credits-and-plans model. It's in active production use as the shared email backbone across most of the studio's products, with a TypeScript rewrite underway alongside the live Elixir service.
Key features
One backend for every product's email
Several small products send through one shared service, so email — the part everything needs and nobody wants to rebuild — is solved once, not per app.
Campaigns and transactional in one
Bulk marketing sends and critical transactional emails run through the same pipeline, so a team manages both from one place instead of two systems.
Tracking and list hygiene built in
Opens, clicks, subscribes, unsubscribes and provider bounce/complaint events are all handled, so a sender's reputation and list stay healthy without manual cleanup.
Newsletter content from feeds
Content 'sources' can pull from RSS, sitemaps and social feeds to seed a newsletter, so recurring sends don't start from a blank page every time.
Webhooks for downstream reactions
Accounts register their own webhooks for email events, so other systems can react to a send, open or bounce as part of a wider workflow.
Where it stands
In active production use across the majority of the studio's apps, sending both campaigns and transactional email. A TypeScript rewrite is progressing alongside the live Elixir service.
Under the hood — for the technically-minded
How it's built
The service is an Elixir/Phoenix API over Postgres, using a Redis-backed background-job system for the send pipeline, and AWS's email stack — SES to send, SQS and SNS to receive delivery events — with S3 for assets. Both send paths, bulk and transactional, deliberately converge on the same worker so there's one place email actually goes out.
It's mature and in active production use. A TypeScript rewrite is underway inside the same parent repository — porting the data model table-for-table and reusing the shell of a former sibling project's frontend as the new dashboard — running alongside, not instead of, the live Elixir service. That in-flight rewrite, and the RSS/social content-sourcing angle, are the more interesting parts of its current story.
The hard problems
One pipeline for two very different jobs
Bulk marketing and transactional email have different urgencies but shouldn't be two separate systems, so both funnel through a single worker — one place to reason about deliverability, retries and provider events, rather than duplicating the hard parts twice.
Closing the loop on deliverability
Sending is the easy half; staying deliverable is the hard half. MailTrooper ingests the provider's bounce and complaint events back through a webhook handler and acts on them, so the list self-cleans and the sending reputation is protected automatically.
A careful language migration
Rather than a rewrite from scratch, the move from Elixir to TypeScript ports the data schema table-for-table and reuses an existing frontend shell — a deliberate, low-risk migration strategy for a service other products depend on, where a clean-break rewrite would be dangerous.
Built with
- Elixir/Phoenix
- Postgres
- AWS SES/SQS/SNS
- Redis
Building something in this space?
Work with us →