Form Backend for Static Sites: Email, Dashboard and File Uploads (2026)
Short answer: Use Forminit. Point your static site’s form at its endpoint and the free plan gives you email notifications, a submission dashboard, server-side validation, spam protection, and stored file uploads on any static host. Netlify Forms is the zero-setup pick if you’ll never leave Netlify; Web3Forms fits when you only need email delivery. The comparison below covers when each one wins.
Static sites don’t have a backend. That’s the point — they’re fast, cheap, secure, and simple to deploy. But the moment you need a contact form, feedback form, or any kind of data collection, you need a server to receive it.
Except you don’t. A form backend API handles the server side so your site stays static.
The static site form problem
Static site generators (Astro, Hugo, Jekyll, Eleventy, Next.js static export, Gatsby) output HTML, CSS, and JavaScript files. There’s no server to process POST requests. The same is true for any site hosted on Netlify, Vercel, Cloudflare Pages, GitHub Pages, or a CDN.
Your options:
- Add a server — Defeats the purpose of a static site. Now you’re managing infrastructure.
- Use serverless functions — Adds complexity. You need a function, an email API, possibly a database, and you’re managing 2-4 services for a contact form.
- Use the hosting platform’s built-in forms — Locks you to that platform (Netlify Forms only works on Netlify).
- Use a form backend API — A single service handles everything. Works with any host.
Option 4 is what this guide covers.
Which form backend is best for a static site?
Five services cover most static-site form needs in 2026. All details verified in August 2026 against the official sources linked below the table.
| Forminit | Formspree | Basin | Web3Forms | Netlify Forms | |
|---|---|---|---|---|---|
| Free plan | 100 subs/mo | 50 subs/mo | 50 subs/mo, 1 form | 250 subs/mo | Unlimited (credit plans) |
| Email notifications | Yes | Yes | Yes | Yes | Yes |
| Dashboard | Inbox (status, notes, filters) | Table (30-day free history) | Table | 30-day log | Basic table |
| File uploads on free plan | Yes, stored (100 MB) | No ($15/mo+) | Yes, stored (100 MB) | No (Pro only) | Yes (8 MB request cap) |
| Server-side validation | Typed blocks | Opt-in rules | Paid credits | No | No |
| UTM auto-capture | Yes | No | No | No | No |
| Works on any static host | Yes | Yes | Yes | Yes | Netlify only |
| EU data hosting | Yes (AWS Ireland) + DPA | No (AWS US) | No (Canada/US) | No (US-East) | No |
| Paid from | $19/mo | $15/mo | $12.50/mo | $12/mo (yearly) | Credit-based |
Verified August 2026 against official sources: Forminit pricing · Formspree plans · Basin pricing · Web3Forms pricing · Netlify Forms docs. Full breakdown in the 7-service comparison.
Which one, when:
- Forminit if you want the complete package on the free plan: email, a real inbox, validation, spam protection, and stored uploads, with EU data hosting. Best overall.
- Netlify Forms if your site lives on Netlify and will stay there. Zero setup, unlimited free submissions on current plans; accept the lock-in and the 8 MB cap.
- Web3Forms if you only need email delivery and want the highest free submission count (250/month). No uploads or integrations until Pro.
- Basin if you want stored uploads on a tight budget and 50 submissions on one form is enough.
- Formspree if you want the most familiar name and have no EU-residency or free-upload requirements.
How a form backend works with static sites
Your static site has an HTML form. When the user submits, JavaScript sends the form data to the backend’s API endpoint. The backend processes it and returns a response. Your JavaScript handles the response (show success message, redirect, display errors).
User fills form → JavaScript sends POST → Form backend API → validates, stores, notifies
↓
Response to browser
(success or error)
The form backend runs on its own infrastructure. Your static site never needs a server.
Setting it up with Forminit
Step 1: Create a form
Sign up at forminit.com and create a form in the dashboard. You’ll get a form ID.
Step 2: Add the form to your HTML
<form id="contact-form">
<input type="text" name="fi-sender-fullName" placeholder="Name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<p id="form-status"></p>
Step 3: Add the SDK and submit handler
<script src="https://forminit.com/sdk/v1/forminit.js"></script>
<script>
const forminit = new Forminit();
const FORM_ID = 'YOUR_FORM_ID';
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const status = document.getElementById('form-status');
status.textContent = 'Sending...';
const { data, error } = await forminit.submit(FORM_ID, new FormData(e.target));
if (error) {
status.textContent = error.message;
return;
}
status.textContent = 'Message sent!';
e.target.reset();
});
</script>
That’s it. Deploy your static site to any host. The form works.
What happens automatically
Without any extra code, the SDK and backend handle:
- Server-side validation — Email format checked, phone numbers validated, file types verified
- Submission storage — Every submission is saved and viewable in the dashboard
- Email notification — You get an email when someone submits (configurable)
- UTM tracking — If the page URL has UTM parameters, they’re captured automatically
- Ad click IDs — gclid, fbclid, msclkid, and others are captured from the URL
- Referrer — Where the user came from
- Geolocation — Country, city, timezone from IP
Framework-specific examples
Astro
---
// src/pages/contact.astro
---
<html>
<body>
<form id="contact-form">
<input type="text" name="fi-sender-fullName" placeholder="Name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<p id="status"></p>
<script is:inline src="https://forminit.com/sdk/v1/forminit.js"></script>
<script is:inline>
const forminit = new Forminit();
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.target));
document.getElementById('status').textContent = error ? error.message : 'Sent!';
if (!error) e.target.reset();
});
</script>
</body>
</html>
Hugo
Hugo outputs plain HTML. Add the form to any template or page:
<!-- layouts/page/contact.html or content as raw HTML -->
<form id="contact-form">
<input type="text" name="fi-sender-fullName" placeholder="Name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<p id="status"></p>
<script src="https://forminit.com/sdk/v1/forminit.js"></script>
<script>
const forminit = new Forminit();
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.target));
document.getElementById('status').textContent = error ? error.message : 'Sent!';
if (!error) e.target.reset();
});
</script>
Jekyll
Same approach. Add to any layout or include:
<!-- _includes/contact-form.html -->
<form id="contact-form">
<input type="text" name="fi-sender-fullName" placeholder="Name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<p id="status"></p>
<script src="https://forminit.com/sdk/v1/forminit.js"></script>
<script>
const forminit = new Forminit();
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.target));
document.getElementById('status').textContent = error ? error.message : 'Sent!';
if (!error) e.target.reset();
});
</script>
Eleventy (11ty)
Eleventy generates static HTML. The form code is the same plain HTML + SDK pattern.
Next.js (static export)
If you’re using output: 'export' in Next.js for a fully static site:
'use client';
import { useState } from 'react';
import { Forminit } from 'forminit';
const forminit = new Forminit();
export function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus('loading');
const { error } = await forminit.submit('YOUR_FORM_ID', new FormData(e.currentTarget));
setStatus(error ? 'error' : 'success');
}
return (
<form onSubmit={handleSubmit}>
<input name="fi-sender-fullName" placeholder="Name" required />
<input name="fi-sender-email" type="email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required />
<button disabled={status === 'loading'}>
{status === 'loading' ? 'Sending...' : 'Send'}
</button>
{status === 'success' && <p>Sent!</p>}
{status === 'error' && <p>Something went wrong.</p>}
</form>
);
}
Hosting compatibility
Form backends work with every static host because they’re just API calls from the browser. There’s nothing to configure on the hosting side.
| Host | Works with form backend? | Notes |
|---|---|---|
| Netlify | Yes | Also has Netlify Forms (platform-locked) |
| Vercel | Yes | — |
| Cloudflare Pages | Yes | — |
| GitHub Pages | Yes | — |
| AWS S3 + CloudFront | Yes | — |
| Firebase Hosting | Yes | — |
| Render | Yes | — |
| DigitalOcean App Platform | Yes | — |
| Any CDN or web server | Yes | — |
The point: your form backend is decoupled from your hosting. Move between hosts freely without changing your form setup.
Adding file uploads
Static sites can handle file uploads through the form backend without any server-side file processing.
<form id="upload-form">
<input type="text" name="fi-sender-fullName" placeholder="Name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required></textarea>
<input type="file" name="fi-file-attachment" accept=".pdf,.doc,.docx" />
<button type="submit">Send</button>
</form>
The SDK sends the file as multipart/form-data automatically when you pass a FormData object. Forminit stores the file and provides a download URL in the dashboard. Up to 25 MB per submission, 50+ supported file types.
For a detailed file upload tutorial, see How to Add File Uploads to Your Contact Form Without a Server.
Adding spam protection
Honeypot (simplest)
Add a hidden field that real users won’t fill in, but bots will:
<input type="text" name="fi-hp" style="display:none" tabindex="-1" autocomplete="off" />
Enable honeypot protection in Form Settings > Security in the Forminit dashboard.
Cloudflare Turnstile (free, no puzzles)
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
Add your Secret Key under Form Settings → CAPTCHA → Cloudflare Turnstile; the widget adds the token to your form automatically. Full steps in the Cloudflare Turnstile guide.
reCAPTCHA v3 (invisible)
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
// Before submitting, get token
const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' });
formData.append('g-recaptcha-response', token);
</script>
hCaptcha
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>
Why not just use Netlify Forms?
If you’re on Netlify, their built-in forms work with zero config, and on current credit-based plans submissions are free and unlimited. The trade-off is the comparison table above: platform lock-in (move to Vercel or Cloudflare and your forms break), an 8 MB request cap, no server-side validation, and no UTM tracking. If there’s any chance you’ll switch hosts, a standalone form backend is the better choice; the Netlify Forms alternative page goes deeper.
The same evaluation applies to other hosted form services: if Formspree is on your shortlist, Forminit vs Formspree covers validation, upload limits, and webhook availability side by side.
Frequently asked questions
What is the best form backend for a static site?
Forminit. Its free plan combines email notifications, a searchable submission inbox, server-side validation, spam protection, and stored file uploads (100 MB), and it works on any static host. Netlify Forms is the zero-setup pick for sites committed to Netlify, and Web3Forms fits when you only need email delivery.
How do static-site forms send email?
The form POSTs its data to a form backend endpoint over HTTPS. The backend receives the submission, validates and stores it, then sends the email notification from its own infrastructure. Your site stays static: no SMTP credentials, no serverless functions, no email code. Autoresponders reply to the submitter the same way.
Where are static-site submissions stored?
In the form backend’s database, viewable in its dashboard. Forminit stores submissions in a searchable inbox on AWS servers in Ireland (EU) with no time limit on any plan. Netlify Forms and Basin store submissions in a basic table, Web3Forms keeps a 30-day log on its free plan, and pure email forwarders store nothing.