June 17, 2026 · cloudflare · edge · email · anti-bot · engineering

A contact form with no third parties: Turnstile, a Worker, and email on the edge

Putting mailto:you@domain.com on a page is handing your inbox to every scraper on the internet. The polite alternative — a contact form — has a catch on a static site: there’s no server to receive the POST. So most people reach for a third-party form service, and now every message a stranger sends you also lands in some SaaS company’s database.

I wanted a contact form with three properties: no address in the HTML, no bots, and no third party that gets to read my mail. Here’s the shape I landed on, entirely on Cloudflare’s free tier.

The shape

A static page (the form) → Cloudflare Turnstile for the bot check → a Cloudflare Worker that verifies the challenge and sends the mail through Cloudflare Email Sending. Nothing leaves my own account.

The non-obvious parts

Pages Functions can’t send email. This bit me. The site is hosted on Cloudflare Pages, and Pages Functions are almost a Worker — but the email binding is Workers-only; it isn’t in the Pages binding set at all. The fix is a standalone Worker bound to a route on the same domain (/api/contact), so the form posts same-origin — no CORS, no exposed *.workers.dev URL — and the Worker, not Pages, answers that one path.

A Turnstile token means nothing until the server checks it. The widget on the page produces a token; the value is in verifying it server-side against Cloudflare before you trust the submission. The page can be faked; the siteverify call can’t.

A honeypot is the cheap first filter. A hidden field no human ever sees. If it arrives filled, it’s a bot — drop it silently, before spending a Turnstile verification or an email send on it.

Geolocation is free at the edge. Every request carries a cf object — IP, city, region, ASN/ISP, the exact Cloudflare datacenter the visitor entered through, latitude and longitude. The notification I get includes all of it plus a one-click map pin. No lookup service, no extra call — it’s just there on the request.

The auto-reply closes the loop. The Worker sends two messages: a notification to me, and an acknowledgment to the sender — “your message landed safely, I’ll reply personally soon.” That reply is addressed From: an address on my domain whose replies route straight back to my inbox, so a “thanks!” from the visitor lands where I’ll see it. Sending to an arbitrary visitor (rather than only my own verified address) is the one capability that pushed this onto the newer Email Sending product instead of the legacy routing send.

What it buys

  • Nothing to harvest — the address exists only in a server-side binding, never in a page a scraper can read.
  • No SaaS in the path — no third party stores, indexes, or mines the messages; no monthly submission cap.
  • Bots filtered twice — honeypot for the lazy ones, Turnstile for the rest, both before anything is sent.
  • Context for free — I see roughly where a message came from without instrumenting anything.
  • $0 — Pages, Workers, Turnstile, and a low volume of transactional email all sit inside free tiers.

The lesson that generalizes: “static site” doesn’t mean “no backend” anymore. An edge Worker on a single route gives you exactly as much server as one job needs — and you get to keep your data, your inbox, and your forty-cent-per-month hosting bill.

Part of a larger system I’m documenting in the HMAS white paper — methods shown, parameters withheld.


All writing