MailFloearly access
← Blog

· 7 min read · by Marek

The doorbell pattern: inbound email behind a firewall

Live Radio Director is the control room behind an internet radio station. It schedules programmes, prepares playlists and runs the AI DJs on air. Every DJ has their own email address on the station's domain, and listeners write in: requests, dedications, stories from the night bus. Those messages are screened and land in the DJ's inbox within seconds, ready to be read out live.

The interesting part is where Live Radio Director runs. It sits on a private network with no public endpoint at all. Nothing on the internet can open a connection to it. That rules out the one delivery mechanism most email providers offer for inbound mail, and it is exactly the case MailFlo's inbound stream was built for. This post walks through the pattern we use to connect the two. We call it the doorbell.

Inbound email usually means a webhook

The standard inbound setup across transactional email providers is the same everywhere: point your MX record at the provider, give it a public HTTPS URL, and every message arrives as a POST. It works well for a production API on the open internet.

It does not work for anything that can't be reached from outside. On-premise software, a box in a studio rack, a home lab, a desktop app, a worker inside a locked-down VPC: none of them can accept that POST. MailFlo also refuses webhook URLs that resolve to private or internal addresses, at save time and again at delivery time, so pointing a webhook at an internal hostname isn't an option either. That guard protects every tenant on the platform.

Teams in this position usually end up opening a port, running a tunnel, or building a public relay that forwards mail inward. Each of those adds a moving part to operate and a new surface to defend, just to learn that an email exists.

The doorbell: ring from outside, answer from inside

A courier doesn't push a parcel through your wall. They ring the bell, and you open the door. The doorbell pattern splits inbound delivery the same way, into two separate jobs:

Both connections start inside the private network. The firewall only ever sees outbound traffic, so there is nothing to open, forward or expose.

MailFlo (public)

1 · Mail lands

Parsed & stored

dj@station.fm - MX points at MailFlo, one durable record per message

2 · Ring

Doorbell event

{ type, id, from_email, subject } - no body, no attachments

firewall · no inbound ports

Your system (private)

Always open · Dial out

Outbound WebSocket

Opened from inside the network with the project API key

3 · Answer

Pull drain

GET /api/v1/inbound?cursor=… - fetch everything new, dedupe, store

Safety net

Timer and cron run the same drain

A missed ring costs at most one interval, never an email

Why a missed ring never loses an email

The doorbell is deliberately best-effort. Sockets drop, processes restart, networks blink. Instead of making the push carry delivery guarantees, the guarantee lives in the pull, and the doorbell only decides when the pull runs.

In Live Radio Director the same drain function has four callers: the doorbell, a 60-second timer inside the subscriber, a once-a-minute cron, and a "Sync now" button. If a ring is missed, the next tick picks the message up. The worst case is a minute of latency, never a lost email. A few details make that hold up under real traffic:

What it looks like in code

A condensed version of the subscriber. The cursor helpers and storage are yours; everything MailFlo-specific is in the two URLs and the event type:

import WebSocket from "ws";

const STREAM = "wss://YOUR_INBOUND_HOST/v1/inbound/stream";
const auth = { Authorization: `Bearer ${process.env.MAILFLO_API_KEY}` };

let draining = false;
let ringAgain = false;

// The one delivery guarantee: page through everything newer than our cursor.
async function drain() {
  if (draining) return void (ringAgain = true); // rang mid-drain: go once more
  draining = true;
  try {
    do {
      ringAgain = false;
      let cursor = await loadCursor();
      for (;;) {
        const res = await fetch(
          `https://console.mailflo.dev/api/v1/inbound?${cursor.query}`,
          { headers: auth },
        );
        const page = await res.json();
        await saveEmails(page.data); // upsert on the MailFlo email id
        cursor = await advanceCursor(page);
        if (!page.has_more) break;
      }
    } while (ringAgain);
  } finally {
    draining = false;
  }
}

function connect(attempt = 0) {
  const ws = new WebSocket(STREAM, { headers: auth });
  ws.on("open", () => {
    attempt = 0;
    drain(); // catch anything that landed while we were away
  });
  ws.on("message", (raw) => {
    const event = JSON.parse(raw.toString());
    if (event.type === "inbound.email.received") drain(); // ding-dong
  });
  ws.on("close", () => {
    const backoff = Math.min(1000 * 2 ** attempt, 60_000) + Math.random() * 500;
    setTimeout(() => connect(attempt + 1), backoff);
  });
}

connect();
setInterval(drain, 60_000); // the safety net

Security comes with the shape

Why you rarely get this out of the box

Most transactional email platforms treat inbound as a webhook feature and stop there. If your consumer can't take a POST, the answer is a tunnel or a relay you build and run yourself, plus a polling loop as your safety net.

MailFlo ships both halves of the doorbell as part of the product: a per-project WebSocket stream for the ring, and a cursor-based inbound API built for idempotent ingestion on your side. Your webhook keeps working alongside them. You pick the path per consumer: the webhook for public services, the doorbell for everything behind a firewall, and the same GET /api/v1/inbound source of truth underneath both. The launch post on push notifications for inbound email covers the stream itself in more detail.

Where else the doorbell fits

Live Radio Director is one shape of a common problem. The same pattern works for on-premise and self-hosted products, support desks inside corporate networks, desktop and CLI tools, local development without a tunnel, and IoT or edge devices that only ever dial out. If it can hold a WebSocket and make an HTTPS request, it can receive email in real time.

Receiving email somewhere a webhook can't reach?

Apply for early access and get inbound running on your own domain.

Request an invite