
A contact form on a static website, without relying on third parties
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.
Why use n8n as a backend for a contact form?
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.
The workflow: stateless, but strict
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:
- The webhook receives a POST request
- Fields are normalized
- Traffic is filtered and validated
- Only valid messages are allowed through
- An email is sent
- The client receives a clean JSON response
Simple in structure. Strict in execution.
Built-in security layers
This form is intentionally not “simple.” A public endpoint should be designed defensively, especially without a traditional backend.
1. IP allowlist
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.
2. Honeypot field
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.
3. Strict field validation
Name, email, and message are validated for:
- minimum length
- valid email format
Invalid input receives a clear 400 response. No vague “something went wrong.”
4. Nonce with expiration
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.
5. Timing-safe signature checks
All cryptographic comparisons use timing-safe methods. No subtle leaks via timing attacks.
Small detail. Big difference.
6. Optional HMAC over the full payload
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.
7. Rate limiting per IP
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.
8. Controlled responses
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.
Why I stopped using external form services
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.
Final thoughts
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.