What

formgate is a single Go binary, nothing outside the standard library, one config file. It sits between static marketing sites and SMTP. A public contact form posts JSON to it; formgate decides whether the submission is worth an email, and if it is, delivers it. The mail path is never exposed to the page. It serves twelve sites right now.

Why

The OTP that amplified spam

Static sites still need contact forms, and contact forms need mail. The question is how the form sends mail without putting SMTP credentials inside a public page.

Our first answer was an emailed OTP. The visitor filled the form, then had to stop, open their inbox, find the email, copy the code, come back, and type it in. Twelve forms used it.

Real leads did not finish that round trip. A person who wanted to send an enquiry was forced to leave the page and hunt for an email before the form would do anything, and most of them simply gave up. The bots never cared. The friction did not stop them from submitting, because the damage they did had nothing to do with completing the step.

The worse failure was invisible for a while. Every bot submission made the system send an OTP email to whatever junk address the bot had filled in. The anti-spam measure had become an outbound email amplifier. Bots could make us send mail on demand, and every one of those sends counted against the domain’s sending reputation.

That is the rule the whole redesign follows: a defense that sends mail on demand is a mail-sending loop waiting to happen.

Turnstile, in the right slot

Cloudflare Turnstile replaced the OTP. The visitor solves a challenge in the browser, the site sends the token with the form, and the server redeems it. No email round trip, nothing to type back.

Turnstile is a bot filter. It is not a rate limiter and not DDoS protection, and treating it as either is the common mistake. Repeated submissions from one person or a token farm are still possible, and volumetric abuse belongs at the edge, in a WAF. In formgate, Turnstile occupies exactly one slot: deciding whether the submission came from a browser run by a person.

The open relay problem

The second half of the design is about what the gateway is allowed to send.

A naive form gateway accepts a body and a recipient and sends the mail. That is an open relay: anyone who finds the endpoint can use your SMTP to mail anyone. Spammers find those.

formgate is closed by construction. The sender, the recipient, and the subject come only from the server-side config file. The visitor contributes two things: the message text and their own address, which becomes the Reply-To. It never becomes the envelope sender.

That last choice is what keeps SPF and DKIM aligned. The envelope sender is the site’s configured from address, so the mail checks out against the domain it claims to be from. The Reply-To points at the team inbox, so a human reply lands next to the original enquiry. Bounces and vacation responders go to the noreply address, not the team.

Choosing the direction of every failure

Every step in a pipeline has to fail in some direction. The interesting design decision is picking which one, step by step.

  • Turnstile fails closed. A failed verification is dropped before it costs a database lookup or an SMTP connection. Dropping a bot costs nothing, so there is no reason to be generous.
  • The bad-email lookup fails open. If the service that checks disposable and bounced addresses is unreachable, the submission still goes through. Dropping a real enquiry costs more than mailing a questionable address, and the ESP still refuses genuinely undeliverable recipients at its end. A blocked verdict is honoured; only an unreachable service is waved through, and it is logged.
  • The acknowledgement never fails the request. By that point the enquiry is already delivered. Failing the whole request over a failed ack would report failure for a job that succeeded.

Each direction is a bet on which error is cheaper, and the three bets point different ways. That asymmetry is the design.

How

The pipeline runs nine steps in order and stops at the first failure. Conceptually:

  1. Parse the JSON, capped at 64 KB.
  2. Resolve the site name to its server-side config.
  3. Redeem the Turnstile token at Cloudflare.
  4. Check the hostname Cloudflare reports against the site’s allowed hostnames.
  5. Validate the submitter address.
  6. Ask the bad-email service whether the address is safe to mail.
  7. Sanitize the body: UTF-8, size cap, control characters, block patterns.
  8. Deliver the enquiry to the site’s configured recipient.
  9. Send the acknowledgement back to the submitter.

Step 3 sits first on purpose, so the cheapest rejections happen before anything with a cost.

One concept worth naming before the code: token expiry is not the same as verification failure. Turnstile tokens live 300 seconds and are single use, so a visitor who leaves a tab open arrives with a stale token. That is not a bot. The gateway distinguishes the two cases and lets the browser silently resubmit once with a fresh token, instead of making a human retype the form.

Adding a site is a config entry plus a Turnstile widget. No code change.

This Series

The posts in this series: