RG Forms logoRG Forms
Architecture

Contact form without a backend

“No backend” sounds like a compromise, as though you’re doing without something. It’s worth being precise about what a backend was actually doing for a contact form — because for this specific job, the list is short and every item on it has a good answer.

Last updated August 19, 2026

The short answer

Running a contact form without a backend means replacing the five jobs a server was doing — receiving the POST, validating it, storing it, notifying you, and blocking spam — with services that already exist. A hosted form endpoint covers all five. RG Forms covers them using infrastructure inside your own Google account: an Apps Script web app receives the POST, appends a row to your Google Sheet, emails you from your own account, and verifies a Turnstile token before saving. You maintain no server, no database and no dependencies, and you still own every submission.

The five jobs, and who does them now

The jobWith a server you runWithout one (RG Forms)
Receive the POSTYour route handler, on a host you pay forAn Apps Script web app in your Google account, at a permanent HTTPS URL
Validate the dataServer-side validation code you wroteHTML validation in the browser, plus whatever rules you add to the script
Store itA database you provision, back up and patchA row in a Google Sheet you already own
Notify youAn email API with its own key and billMailApp sending from your own Google account, with reply-to set to the sender
Block spamRate limiting and a captcha you wire upA honeypot field, plus optional Cloudflare Turnstile verified server-side

The thing you’re really avoiding is operations

Writing the endpoint was never the hard part — it’s twenty lines. The cost is everything that comes after: a host that has to stay up, a runtime that goes end-of-life, dependencies with security advisories, a database that needs backups, a certificate that expires, and a monthly bill for a service that receives four messages a week.

For a contact form on a brochure site, that ratio is indefensible. The whole point of going serverless here is to delete the operational surface, not to be clever.

What “no backend” costs you

Being honest about the trade: because there’s no server of yours in the path, required-field enforcement is a frontend concern. A determined person can POST directly to the endpoint and skip your HTML validation. For a contact form that’s a non-issue — the worst case is a row with empty cells — but it’s the wrong architecture for anything that needs to be authoritative, like a payment or an account signup.

Two other things worth knowing: there are no file uploads (text fields only), and there is no read API — the endpoint accepts submissions but never returns them, so pulling your data into another system means reading the Sheet rather than fetching a URL.

Everything else you’d expect — timestamps, multiple forms, custom fields, notification routing, CC/BCC, custom subjects, spam filtering — is there.

The entire client side

This is the complete integration — there is no server-side counterpart to write.

index.html
<form id="contact-form">
  <label>Name <input name="name" required /></label>
  <label>Email <input type="email" name="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>

  <!-- Honeypot: humans never see it, bots fill it in -->
  <input type="text" name="_hp" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px" aria-hidden="true" />

  <button type="submit">Send</button>
  <p id="form-status" role="status"></p>
</form>

<script>
  const ENDPOINT = "https://script.google.com/macros/s/AKfycb.../exec";
  const form = document.getElementById("contact-form");
  const status = document.getElementById("form-status");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    status.textContent = "Sending…";

    const res = await fetch(ENDPOINT, {
      method: "POST",
      // text/plain avoids the CORS preflight Apps Script can't answer
      headers: { "Content-Type": "text/plain" },
      body: JSON.stringify({
        tab: "contact",
        fields: Object.fromEntries(new FormData(form)),
      }),
    });

    const data = await res.json();
    status.textContent =
      data.result === "success" ? "Thanks — we'll be in touch." : "Something went wrong.";
    if (data.result === "success") form.reset();
  });
</script>

Common questions

Is a serverless function a “backend”?

Functionally yes — it’s your code, running on demand, that you own and maintain. It’s a great choice when you need custom logic. It’s overkill when all you need is “put this message somewhere I’ll see it.”

Can I add server-side validation later?

Yes — the Apps Script is in your own account and fully editable. Open it in the Apps Script editor and add whatever rules you want before the row is written.

Is it slower than a real backend?

The first request after a period of inactivity takes roughly 800ms–2s while Apps Script cold-starts; subsequent ones are fast. For a contact form nobody notices, especially with a “Sending…” state on the button.

What happens if Google is down?

The submission fails, same as any hosted service having an outage. Catch the error and show a fallback email address so the visitor still has a way to reach you.

More answers in the full FAQ.

Your endpoint goes live in ~90 seconds