A contact form on a static website, without relying on third parties

720 words, 4 minutes reading time
By: Sjoerd Blom
Sjoerd Blom Sjoerd Blom
Sjoerd Blom is married and proud father of two daughters. He loves good food, travel, and technical gadgets. Sjoerd mainly writes about the world, travel, and tech.

Static websites have many advantages. They are fast, secure, easy to host, and simple to maintain. With a generator like Hugo, you can deploy a blazing-fast site without a database or complex backend.

But there is one classic pain point: the contact form.

Without server-side logic, you quickly end up relying on third-party services such as Formspree, Getform, or the built-in forms functionality of Netlify. They work. Often even for free or at low cost.

Still, something about that never sat right with me.

You hand over control of your data.

An external party decides:

  • whether a form submission is accepted
  • when something is considered spam
  • how and where your data is stored
  • what metadata is logged

It feels odd that a third party determines whether someone is allowed to contact me.

So I re-enabled my contact form. Fully self-hosted. The core: n8n.


n8n is primarily a workflow automation tool, think of it as an open-source alternative to Zapier. Because you can self-host it, it also works perfectly as a lightweight backend for a static site.

What makes it appealing to me:

  • self-hosted
  • fully transparent
  • no mandatory database
  • highly flexible
  • complete control over logging, validation, and security

My contact form posts directly to an n8n webhook. Everything that happens next is entirely under my control. No black box. No hidden “anti-spam scoring.” No sudden policy changes.


I deliberately designed the workflow to be stateless. No database, no external storage, no sessions. Everything is validated at the moment the request comes in.

I’ll share the workflow JSON soon, once I’ve properly tested it in practice. But at a high level, this is how it works:

  1. The webhook receives a POST request
  2. Fields are normalized
  3. Traffic is filtered and validated
  4. Only valid messages are allowed through
  5. An email is sent
  6. The client receives a clean JSON response

Simple in structure. Strict in execution.


This form is intentionally not “simple.” A public endpoint should be designed defensively, especially without a traditional backend.

Only traffic from known IP addresses is accepted. Everything else immediately receives a 403 response.

Suitable for:

  • your own websites
  • your own servers
  • staging environments

No known origin? No access.

The form includes a hidden field that normal users never fill in. Bots often do.

The result:

  • no visible error
  • an “ok” response
  • no further processing

Bots don’t get feedback that they’ve been caught, which makes continued attempts less attractive.

Name, email, and message are validated for:

  • minimum length
  • valid email format

Invalid input receives a clear 400 response. No vague “something went wrong.”

Each form submission includes:

  • a nonce
  • an expiry timestamp
  • an HMAC signature

The nonce:

  • is time-bound
  • is protected by cryptographic verification
  • requires a server-side secret

This prevents replay attacks and scripted spam.

All cryptographic comparisons use timing-safe methods. No subtle leaks via timing attacks.

Small detail. Big difference.

In addition to the nonce, the entire payload can be signed. This is particularly useful when the form is used from multiple frontends.

It guarantees the content hasn’t been altered in transit.

Each IP address is limited to a maximum number of requests per hour.

Implemented using n8n workflow static data. So:

  • no separate database
  • no Redis
  • no additional infrastructure

Still effective.

Each failure condition has its own HTTP status code:

  • 401 for authentication issues
  • 403 for IP blocks
  • 429 for rate limiting

No generic error pages. Predictable behavior.


In short:

  • my data stays mine
  • no dependency on third-party policies
  • no hidden logging
  • no lock-in
  • full visibility into the entire chain

Yes, it requires more thought than pasting an embed script.

But it aligns with how I want to design my infrastructure: intentional, minimal, and under my control, just like choosing a static site with Hugo instead of a traditional CMS setup.


A static website doesn’t have to compromise on functionality or security.

With a tool like n8n, you build exactly what you need. No more. No less.

My contact form is live again. On my terms.

If you find this interesting, you can now actually reach me. Through that very form.