• Aug 20, 2026
  • 6 min read

From Form to First Drip: Testing an Email Pipeline on Production

Unit tests told me each piece worked: the Worker’s confirm and unsubscribe endpoints, the signed links, the drip engine’s send-once ledger. What they couldn’t tell me was whether a real browser on a real site, a real email provider, a real inbox and my homelab would line up. So I ran the whole chain on production — with one address.

This post is that run, hop by hop, with the checks I used and the two things that looked like bugs but weren’t.

The chain

article page ── subscribe ──► forms Worker ──► KV: lead status=pending
                                 └─ Mailjet: confirmation email
inbox ── link ──► GET confirm page ── button ──► POST confirm ──► KV: status=confirmed
homelab hub ── pull (every 15 min) ──► SQLite ──► drip engine ──► Mailjet: welcome email

The pieces are described elsewhere: double opt-in on Workers, the pull-only homelab mirror, the drip engine.

Testing on production without touching visitors

The sites were live with single opt-in. I didn’t want a staging copy of three sites, and I didn’t want visitors to start receiving confirmation emails before the privacy pages described them.

So the Worker decides per submission:

const sites = list(env.DOUBLE_OPT_IN_SITES);
return sites.includes("*") || sites.includes(lead.site)
    || list(env.DOI_TEST_EMAILS).includes(lead.email);

DOUBLE_OPT_IN_SITES is empty. DOI_TEST_EMAILS is a Worker secret holding my Gmail test address. Every other visitor gets the old flow: stored, alert to my phone, “Subscribed”.

Surprise 1: “no email came”

Halfway through I subscribed with a second address of mine. The phone notification arrived, no email did. It looked like the provider was broken.

The lead store said otherwise: that lead had no status at all — single opt-in — because the address isn’t on the allow-list. The safety switch did its job. Lesson: when a test “fails”, check which path the request actually took before debugging the provider. The status column answered it in one command.

Hop by hop, with the check for each

HopWhat should happenHow I checked
Subscribe on a jalcocertech articlebox says “check your inbox”the page itself
Worker stores the leadstatus: pendingmake leads (reads KV directly; status column)
Worker sends the confirmationprovider acceptswrangler tail for errors; ntfy alert on failure
Email arrivesfrom [email protected], Reply-To my real mailboxGmail, incl. spam and Promotions
Open the linkpage with a button, nothing changes yetKV still pending
Press the buttonstatus: confirmedmake leads
Homelab pulllead mirrored with its new statusleadshub status
Drip runenrolled, step 1 sentdocker compose logs hub, leadshub drip-status
Welcome emailarrives with unsubscribe footer + one-click headersGmail, “show original”

Surprise 2: accepted, but held

The first confirmation didn’t arrive although the Worker logged no error. Mailjet had accepted the message and then held it, because the sending subdomain’s ownership wasn’t validated yet — a separate DNS record from SPF and DKIM, which I had placed one level too high (the full story).

Once the domain validated, Mailjet released the held email. I clicked it, pressed the button, and KV showed:

2026-10-10 07:11:39   jalcocertech.com   newsletter  confirmed   (my test address)

The extra GET on the confirm URL after my POST (a reload, or the mail client checking the link) changed nothing — which is exactly why links only show a button and only POST acts.

The drip side

On the homelab I set DRIPS_ENABLED=1, SENDER=mailjet, copied the link-signing secret into the container’s read-only secrets folder, and restarted. The next pull printed:

listed 20 · new 0 · updated 0 · gone 0 · purged 0 · suppressed 0
backup: /data/backups/leads-20261010.db
drip: enrolled 1 · stopped 0 · sent 1 · failed 0

enrolled 1 · sent 1: my confirmed subscription was picked up by the jt-welcome sequence and its first email went out through Mailjet. The welcome email arrived in Gmail. Steps two and three are due three and seven days after enrolment, on the first pull after they’re due.

Why only one person was enrolled

The sequence file has enroll_since = 2026-10-10, and only people confirmed on or after that date are enrolled. Enabling a welcome series must not email everyone who ever subscribed. Combined with the allow-list, the blast radius of this whole test was exactly one inbox: mine.

One line in that log surprised me: suppressed 0. The day before it was 1 — I had unsubscribed my test address from the fossengineer newsletter while testing unsubscribe.

What happened is by design:

  • The suppression list (suppress:<sha256(email)>) is global: “never email this address”, checked before every send by every sender (the hashed suppression list).
  • Confirming a new subscription is fresh, explicit consent, so the Worker deletes the suppression entry.
  • The old fossengineer lead keeps status: unsubscribed, so I’m still not a fossengineer subscriber.

Consent lives on each subscription; suppression is the global brake. Lifting the brake doesn’t revive old subscriptions. Seeing that in a real log was more convincing than the unit test that covers it.

The checklist I’ll reuse

  1. Gate the new flow by recipient (DOI_TEST_EMAILS), not by environment.
  2. Give every lead a visible status and a one-command way to list it.
  3. Verify the sending domain at the provider before the first test, and look at the provider’s message history, not only the API response.
  4. Watch live logs while testing (wrangler tail), and have an alert for failed sends.
  5. Open the confirm link once without pressing the button and check that nothing changed.
  6. Gate drip enrolment on a start date before enabling any sequence.
  7. After the first drip: drip-status, the provider log, and “show original” in the inbox for the List-Unsubscribe headers.
  8. Note every test lead you create; delete them before go-live.

Takeaways

  • Testing on production is fine when the new path is allow-listed to your own address.
  • When a test seems to fail, first find out which path the request took.
  • “Accepted by the provider” can still mean “held”.
  • A start date on drip enrolment keeps the first run to the people you mean.
  • Consent per subscription plus a global suppression switch behaves sensibly even in the odd unsubscribe-then-resubscribe case.

Next: /support/articles/playwright-route-mocking-form-states/.