← All work
MailTrooperTRANSACTIONAL EMAIL API
Developer & infrastructureLive

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 →