Digital transformation baseline · Article 2

The Sheet Is the Database

A consultation request now reaches a Google Sheet through a plain server-side form post, not client-side JavaScript holding a write endpoint in page source. That change was correct in design — and it still shipped with a real gap for one day, found and closed the same day it was found.

Second in the digital transformation baseline series.

1 Durable record A Google Sheet, named in the privacy policy by what it is — not "shared with partners."
Server-side Where the write actually happens The visitor's browser never holds the write endpoint. Works identically with JavaScript off.
1 day Time a real gap was live Found and closed the same day, before the form was ever described as finished.

A contact form is the one place on a brochure site where a stranger hands over information and expects something to happen with it. What happens to it afterwards is usually invisible by design. This build made a point of it being visible instead — to the one person who actually needs to trust it: the visitor filling it in.

Same destination, one hop moved

The consultation request still lands in a Google Sheet. What changed is who calls the endpoint that writes to it.

Where the write call is made from
Approach this project avoided What's actually deployed
Browser JavaScript holds the write endpoint as a literal string and calls it directly on submit. Plain HTML <form>, ordinary POST to contact-handler.php, which calls the endpoint server-to-server.
Requires JavaScript enabled — no JS, no submission, and no explanation to the visitor. Works identically with JavaScript off, per this project's standing rule that a feature needing JS to function at all should be reconsidered.
The write endpoint is exposed to anyone who reads page source. The endpoint never reaches the browser at all.

Three costs the server-side design avoids, named directly rather than left implicit: the form keeps working for anyone without JavaScript; the endpoint never becomes a public write capability, because it never leaves the server; and no Content-Security-Policy exception has to be carved out for a third-party script origin, because the browser never talks to the sheet's provider directly.

Why the sheet comes before the mail

Ordering here isn't cosmetic. The sheet is the database; email is only a notification. Even now that email authentication is confirmed in place for this domain, a delivered email is never fully guaranteed — so the sheet write stays the record of truth, and the notification email stays a best-effort layer on top of it. If the sheet write succeeds, the enquiry survives regardless of whether the notification ever lands anywhere.

Found the same day, fixed the same day

The right architecture still needs the right check on the other end of it.

The gap that mattered, and how long it lasted

Moving the call server-side closes the most visible weakness — an endpoint sitting in plain text in the browser. It does not, by itself, guarantee the endpoint checks who's calling it. While wiring this up, that's exactly what was missing: the deployed endpoint accepted any request and wrote it straight into the sheet, with no check that it came from this site's own form handler.

That's a real gap, not a theoretical one — a handful of junk rows already sitting in the sheet at the time it was found are consistent with it having been reachable for a while, though there's no way to tell from the sheet alone whether that was the maintainer's own testing or something else finding the endpoint first.

Fixed the same day. The endpoint now rejects any request whose shared secret doesn't match a value held only in the script's own settings and in server-side configuration that never ships with a deploy. A real submission was sent through the live form immediately afterwards and confirmed landing correctly in the sheet — the fix was verified against a real request, not assumed from reading the change.

One further item came out of the same review: a leftover, unused function in the script would return every row in the sheet — names, emails, message text — to anyone who could reach it, if it were ever wired up. Nothing currently calls it, so it isn't an active exposure, but dead code with that shape shouldn't sit in a script anyone might later edit without knowing what it does. Flagged for removal rather than left as a footnote.

Named, not implied

The privacy policy says where a submission goes, by name — because it can, and because a form wired the way the earlier design was couldn't have said it truthfully.

The site's privacy page states plainly that a submission lands in a Google Sheet only the company can open, with a notification copy attempted by email. Not "may be shared with service providers." The specific mechanism, in the same document that also states — accurately, as of this build — that the site sets no cookies, runs no analytics, and loads nothing from a third party the visitor didn't explicitly submit data to.

That specificity has a cost: the moment anything changes — a chat widget, an embedded map, an analytics tag — the privacy page has to change in the same commit, or it starts making claims that are no longer true. The project treats that as a hard rule, not a maintenance chore.

What this doesn't settle

  • No rate limiting on the form handler yet — length caps on each field bound the size of abuse, not the rate of it. Deferred deliberately: the obvious fix is per-IP throttling, and this form's privacy design specifically doesn't collect visitor IP addresses, so adding one needs a disclosure change in the same commit, not a quiet implementation.
  • The dead function noted above is flagged, not yet deleted. Not an active risk today, but worth closing out rather than leaving as a known loose end.
  • The shared secret is a manual, one-time setup step per host — generated, pasted into the script's own settings, then cleared from anywhere else it briefly existed. That's a process a person has to get right once; it isn't self-enforcing.

The through-line

The improvement wasn't a new capability — both the old design and this one write to the same kind of spreadsheet. It was closing the two different ways a write endpoint can go wrong: keeping it out of the browser entirely, and then actually checking who's allowed to call it once it's server-side. The first without the second would have been half a fix wearing the appearance of a whole one.