// lab log

A spam-protected contact form instead of a public email address

A published email address gets scraped, so I replaced mine with a form. Here's how it filters spam and keeps a message safe even when the email fails.

2 min read

The goal

I wanted people to be able to reach me without my email address sitting on the page for scrapers to collect. Two things mattered: bots should be stopped, and a real message should never disappear.

How a message is handled

A small Cloudflare Worker receives the form. It works in three stages:

  1. Check: several layers of spam protection and validation.
  2. Save: the message goes into a database.
  3. Send: the message is emailed to me, and the database records whether that worked.

The cheap checks come first, so obvious junk never costs a call to another service.

Decisions worth explaining

Layers, not one big check. A trap for simple bots, Cloudflare Turnstile and a rate limit each catch different things, so no single one has to be perfect.

The trap doesn't announce itself. When a simple bot gets caught, it receives a normal-looking reply and nothing is processed. The bot gets no signal to adapt to.

Turnstile is checked on the server. The widget in the browser only produces a token. The Worker has to verify that token with Cloudflare. Otherwise a bot could skip the widget altogether.

The message is saved before the email is sent. If the email service is down, the message is still in the database, marked as not delivered, and I can look it up. The visitor still sees success, because their message is safe.

The database never holds a raw IP address. The rate limit needs some way to recognise a repeat sender, so it stores a keyed hash of the IP instead.

Name and email fields are cleaned up. A visitor can't use them to add extra headers to the email.

What I took away

Save first. Once the message is stored, everything after that step can fail without losing it.

← Back to the lab log