- 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
| Hop | What should happen | How I checked |
|---|---|---|
| Subscribe on a jalcocertech article | box says “check your inbox” | the page itself |
| Worker stores the lead | status: pending | make leads (reads KV directly; status column) |
| Worker sends the confirmation | provider accepts | wrangler tail for errors; ntfy alert on failure |
| Email arrives | from [email protected], Reply-To my real mailbox | Gmail, incl. spam and Promotions |
| Open the link | page with a button, nothing changes yet | KV still pending |
| Press the button | status: confirmed | make leads |
| Homelab pull | lead mirrored with its new status | leadshub status |
| Drip run | enrolled, step 1 sent | docker compose logs hub, leadshub drip-status |
| Welcome email | arrives with unsubscribe footer + one-click headers | Gmail, “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.
Insight: consent is per subscription, suppression is global
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
- Gate the new flow by recipient (
DOI_TEST_EMAILS), not by environment. - Give every lead a visible status and a one-command way to list it.
- Verify the sending domain at the provider before the first test, and look at the provider’s message history, not only the API response.
- Watch live logs while testing (
wrangler tail), and have an alert for failed sends. - Open the confirm link once without pressing the button and check that nothing changed.
- Gate drip enrolment on a start date before enabling any sequence.
- After the first drip:
drip-status, the provider log, and “show original” in the inbox for theList-Unsubscribeheaders. - 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/.