The contact form backend we build for every client site
Every small business site needs a contact form, and most of them break quietly. The owner finds out weeks later, when a customer mentions they never got a reply. We first built this backend in May 2026 for a Montana PR firm's consultation form, and we have reused it on every site since. It has one job: no silent losses.
The shape
- The browser posts JSON to a webhook. That is all the frontend knows.
- An n8n workflow validates the fields, rejects bots, and stamps the time.
- The record inserts into Postgres.
- A notification email lands in the owner's inbox.
- The visitor sees the thank-you state only after the whole chain succeeds.
You can rent roughly this from a form service for a monthly fee per site. We build it once instead. The lead lands in a database we control, so the client can add a dashboard, an auto-reply, or a CRM sync later without moving platforms. And when something breaks, we can see exactly which link in the chain failed.
The 5 rules, and the failure behind each one
1. Send a real submission and read the actual email
In n8n templating, ={{ $json.name }} puts the customer's name in the notification email. ={ $json.name } with single braces also compiles, runs green, and sends mangled emails. No error message told us. The first live test did. A green workflow run proves the workflow ran, not that the output is right.
2. An unreachable database does not fail. It stalls.
We once pointed the insert at a database host the workflow could not reach. Nothing errored. The node hung until the workflow timed out, and the visitor watched a spinner. Test the insert from the machine that will actually run it, not from your laptop. They do not always see the same network.
3. A honeypot beats a CAPTCHA for the small stuff
Add a hidden field named website. Humans never see it. Bots fill everything. If it arrives non-empty, return 200 with an empty body. The bot believes it succeeded and moves on, and no human ever solved a puzzle to send you a message.
4. Timestamps come from the server
A visitor's computer clock can be wrong by minutes or years. The workflow sets created_at at insert time. Never store the clock of a machine you do not control.
5. The browser gets one URL and zero keys
The frontend knows the webhook address and nothing else. Database credentials live in the automation platform. If a key would ship in client-side JavaScript, the design is wrong, full stop.
The last step does not automate
Good bot protection blocks your own robots too. We run Cloudflare Turnstile on our forms, and it refuses headless browsers no matter how we drive them. That is the tool doing its job. So every form deploy ends the same way: a human fills out the real form and reads the real email. There is no substitute, and we have stopped trying to invent one.
Steal the pattern. The tools are swappable. The rules are not.