New: Cloudflare Turnstile CAPTCHA is now available. ->
Back to all posts

Form Backend for Static Sites: Email, Dashboard and File Uploads (2026)

By Forminit in Guides • Updated August 10, 2026 • Published February 20, 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:

  1. Add a server — Defeats the purpose of a static site. Now you’re managing infrastructure.
  2. 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.
  3. Use the hosting platform’s built-in forms — Locks you to that platform (Netlify Forms only works on Netlify).
  4. 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.

ForminitFormspreeBasinWeb3FormsNetlify Forms
Free plan100 subs/mo50 subs/mo50 subs/mo, 1 form250 subs/moUnlimited (credit plans)
Email notificationsYesYesYesYesYes
DashboardInbox (status, notes, filters)Table (30-day free history)Table30-day logBasic table
File uploads on free planYes, stored (100 MB)No ($15/mo+)Yes, stored (100 MB)No (Pro only)Yes (8 MB request cap)
Server-side validationTyped blocksOpt-in rulesPaid creditsNoNo
UTM auto-captureYesNoNoNoNo
Works on any static hostYesYesYesYesNetlify only
EU data hostingYes (AWS Ireland) + DPANo (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.

HostWorks with form backend?Notes
NetlifyYesAlso has Netlify Forms (platform-locked)
VercelYes
Cloudflare PagesYes
GitHub PagesYes
AWS S3 + CloudFrontYes
Firebase HostingYes
RenderYes
DigitalOcean App PlatformYes
Any CDN or web serverYes

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.

Further reading