Explainer
What is a form backend, and when does a static site need one?
A form backend is the server-side half of a form: the part that receives the submission, decides whether it is acceptable, stores it, tells someone it arrived, and keeps a record of what happened next. It is not the form itself, and it is not a form builder — it has no editor and does not care what your markup looks like.
That definition is short, and almost every confusion about static sites comes from collapsing one of its clauses. Strip "stores it" and you have a mailto link. Strip "records what happened" and you have a form that looks finished and silently loses messages. Strip "decides whether it is acceptable" and you have a public endpoint that accepts whatever a script sends, including a thousand times a minute.
The second half of the question — when a static site needs one — is a decision, not a definition, and it has both false positives and false negatives. Some sites bolt on a backend they do not need; others ship a contact form that was never going to work. This article takes the definition apart clause by clause, then gives the signals in both directions.
The five things it has to do
Every one of these happens on a server. All of them are invisible to the visitor, and each is a place where a submission can disappear.
| Stage | What it actually means | What breaks without it |
|---|---|---|
| Receive | Accept a POST in one of the three payload shapes — urlencoded, multipart, or JSON — and parse it into fields. | Static hosting answers with 405 or a 404 page; nothing is captured. |
| Validate | Check required fields, types, and lengths on the server, then apply the owner's rules (block, route, require). | Client-side checks are bypassed by anyone with curl; junk and half-filled records get stored. |
| Store | Write a durable record with the field values, metadata, and attachments. | If delivery fails, the submission is gone. There is nothing to retry and nothing to read. |
| Notify | Send the owner email, the submitter's autoresponse, and any webhook — each as a separate channel. | One channel failing takes the others down, or fails invisibly. |
| Observe | Record a per-channel status, retry with backoff, expose the error, allow a replay. | "The form works" becomes an assumption held until a customer says otherwise. |
The first three stages are what people mean when they say "backend". The fourth is what they mean when they say "it works". The fifth is the one almost nobody evaluates before choosing, and the one that decides whether you find out about a failure from a log or from a person.
Why a static site cannot be its own backend
Static hosting is a file store with a web front end. It does exactly three things: hold files, serve them with cache headers, and map URLs to paths. There is no process reading the request body, no place to write a record, and no connection to a mail server.
This is not a limitation that a clever trick works around. On GitHub Pages a POST is refused. On an S3 bucket, POST is not part of what the bucket understands. On a Cloudflare Pages project with no functions, the asset store answers and the body is discarded. The response looks like a 405, a 404, or an HTML page, and all three mean the same thing: you sent data to something that only knows how to read files.
What static hosting does cover, and this surprises people who expect a total loss, is the entire visitor-facing half of a form: markup, labels, autocomplete, client-side hints, a redirect to a thank-you page, and an inline success message. None of that needs a server. The line is exactly at "hold the data", and everything before that line survives.
The decision: six signals you need one
A static site needs a form backend when a visitor submits data and a person or system has to receive it. That is the whole rule. The useful work is recognising which of the following you are actually in.
1. Someone has to be told. A quote request, a contact message, a job application. If the answer to "what happens after submit" is "an email arrives somewhere", you need the thing that sends that email.
2. The data has to outlive the session. A support ticket, an application with attachments, a record you might need to produce in six months. Files and history both require durable storage.
3. More than one destination has to hear about it. The moment a submission needs to reach an inbox and a spreadsheet, or a CRM and a Slack channel, something has to fan out, retry, and keep a per-destination status.
4. The form is exposed to strangers. A public form gets automated junk within hours of being indexed. Filtering, rate limiting, and captcha all have to run where the request lands.
5. Submissions need to be routed by content. "Send wholesale enquiries to sales and everything else to support" is logic that runs before delivery, which means it runs on the server.
6. Someone will ask a question about a specific submission. "Did Grace's form from Tuesday arrive?" is unanswerable without a record and a delivery status — and it is the question that turns a form from a feature into an operational dependency.
The decision: four signals you do not
Backends get added where they are not needed too, usually because the decision was made by habit rather than by requirement.
1. The site has no forms. Documentation, a landing page, a portfolio. Nothing to receive.
2. The form already posts to something you run. If you have an existing API that accepts the payload, that API is the backend. Adding a second one in front of it adds a failure mode, not a feature.
3. The destination already accepts posts. Newsletter platforms and CRM products accept form posts directly. If the only requirement is "get this address into that list", their endpoint is the backend and yours would be a proxy.
4. The "form" is a mailto link and the stakes are genuinely low. An internal page asking three colleagues for feedback can use a mailto link. Be honest about the trade: no record, no delivery confirmation, and a chunk of visitors with no configured mail client who simply give up. Low stakes makes that acceptable; it does not make it good.
What a form backend is not
Four things get conflated with it, and the conflation is where most product comparisons go wrong.
It is not a form builder. A builder owns the form: its editor, its design system, its markup. A backend owns what happens after submit and deliberately has no editor. Products in this category differ on exactly this axis — the ones with editors ask you to replace your markup, the ones without ask you to keep it.
It is not a CRM. Recording that a submission arrived is not the same as managing a relationship with the person who sent it. A backend that promises pipeline management is a different product wearing the same word.
It is not an email service. It hands messages to a transactional email provider; it does not run a mail server. That distinction matters for deliverability, because the provider's domain reputation and authentication records are doing the real work.
It is not the browser. Client-side validation is a courtesy to the visitor. It is trivially bypassed, so anything that has to be true about the data must be true on the server.
Three ways to get one
Once you have decided you need a backend, there are three shapes, and they differ less in how you connect the form than in what you own afterwards.
| Shape | What you write | What you own afterwards | Good at |
|---|---|---|---|
| A hosted backend | One absolute URL in your form | An account, and a dependency | Intake that should work next week without maintenance |
| Your own function | Handler, storage, mail API, retries, spam handling | All of it, including the 2am failure | Submissions that are operations inside your own system |
| A self-hosted service | Deploy, database, mail, upgrades, backups | The infrastructure and the on-call | Data residency you must control at the hardware level |
The honest way to choose is to ask what the submission is. If it is intake — a message, a request, an application — you are looking for reliability, not code. If it is an operation — creating an order, reserving stock, charging a card — it belongs inside the system that owns that state, and a hosted backend will feel like a wall as soon as the logic gets interesting.
Connecting one, in full
The integration is one attribute. Everything else about the form is yours.
<form action="https://freelyform.com/f/FORM_ID" method="POST">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required>
<label for="email">Email</label>
<input id="email" name="email" type="email"
autocomplete="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Send</button>
</form>
Two details decide whether this works in production. Every field that should arrive needs a name, because unnamed inputs are invisible to the server. And the action must be absolute — a relative path means the POST goes to your static host, which is the failure described earlier.
If you want to stay on the page instead of navigating to a success page, that is a client-side change: post with fetch, render the response, disable the button while the request is in flight. It is worth doing, and it is worth doing second — the native form is the version that cannot break.
<script>
const form = document.getElementById('contact');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const button = form.querySelector('button[type="submit"]');
button.disabled = true; // stops the double submit
const response = await fetch(form.action, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(Object.fromEntries(new FormData(form)))
});
// A 200 means accepted, not delivered — read the endpoint's body, do not
// assume. The delivery record lives in the backend, not in this response.
const result = await response.json().catch(() => ({}));
document.getElementById('status').textContent = response.ok
? 'Thanks — it arrived.'
: (result.message || 'That did not go through. Please try again.');
button.disabled = false;
});
</script>
Six questions that separate a real backend from a convincing one
The setup takes twenty minutes; the decision should be made on what happens in month seven.
- Is delivery recorded per channel? One status per destination, with the provider's message id and the error text. A single "sent" flag hides which channel failed.
- Are retries automatic and visible? A retry you cannot see is indistinguishable from a drop.
- Does the archive expire? A thirty-day window is a data-loss schedule if the form is a record. Ask what happens to a submission from last quarter.
- What happens at the monthly allowance? The good answer is that submissions keep being stored and can be released later. The bad answer is that visitors start being rejected, which loses the lead and hides the reason.
- Can you export and delete? CSV out and per-submission delete in, without a support ticket.
- Can you change backend without touching the markup? Standard HTML means the endpoint is a one-line edit. Provider-specific attributes mean migration is a rebuild.
Five misconceptions worth clearing
"A 200 means it was delivered." It means the request was accepted. Delivery is a later event with its own failures, and the record either shows them or it does not.
"Validation on the form is enough." The browser's required attribute is a suggestion. Anything that must be true has to be checked after the payload leaves the browser.
"Spam filtering is a captcha." A captcha is one signal among several. Honeypots, timing, link density, and rate limits catch most junk without putting a puzzle in front of a real visitor.
"Attachments are just files." Files add a storage location, a type allowlist, a size cap, a retention window, and a URL that must not be guessable. That is five decisions, not one.
"Any endpoint will do, they are all the same." They are the same until something fails. The difference shows up in the record you read afterwards.
Where to go next
- Static sites without server code — the mechanics: payload formats, CORS, and the lifecycle after the 200.
- Contact forms on GitHub Pages, Cloudflare Pages and Netlify — the same decision, platform by platform.
- Features — what this service does in each of the five stages.
- The contact form pattern — field-by-field, including what not to ask.
- Plan ladder — what a free tier includes before you outgrow it.
Form backend questions
Is a form backend the same thing as a form builder?
No. A form builder gives you a visual editor and generates the markup; it owns the form. A form backend owns what happens after submit — receiving, validating, storing, notifying, and recording delivery. A hosted backend deliberately has no editor: you keep the HTML you already wrote.
Does a static site always need a form backend?
Only if a form has to send something to someone. A site with no forms needs nothing; a site whose forms post to an API you already run needs nothing; a site that only displays information needs nothing. The need appears the moment a visitor has to submit data and someone has to receive it.
Can a static site be a form backend for itself?
Not in the sense that matters. Static hosting returns files; it has no process to read a request body, store it, or send mail. Whatever receives the submission has to live somewhere else, even when it is on the same domain.
What is the smallest useful form backend?
Something that accepts a POST, validates it against the form owner’s rules, stores the payload durably, tells the owner it arrived, and keeps a record of whether that notification actually succeeded. Storage and the delivery record are the two parts people skip, and they are the two parts that make the difference between a form that works and a form that appears to work.
When is a serverless function a better choice than a hosted backend?
When the submission is really an operation inside your own system: it must join a transaction with business data, enforce authorization you already model, or trigger orchestration with its own ordering guarantees. For plain intake — contact, quote, application, waitlist — a function is usually one more thing to maintain for the same result.
How do I know a form backend is actually delivering?
Look for a per-channel delivery record: one status per destination, with the provider message id and the error text when a send fails, plus retries you can see and a replay control. A 200 response proves the endpoint accepted the request; it says nothing about whether an inbox received anything.