Skip to content
SheetLink Forms

Product · 2026-08-21 · 8 min read · By Arden Talbot, founder of SheetLink

A beta diary: shipping SheetLink Forms

What we built first, what we deliberately delayed, and the decisions that survived contact with real submissions. An honest, high-level account of the beta that got us here.

A ledger-styled illustration of a diary page whose ruled lines become spreadsheet rows, with a small endpoint URL stamped at the top.

Why write this down

Most products narrate their beta in retrospect, after the rough edges have been sanded into a founding myth. We would rather write the honest version while the memory is still fresh: what shipped first, what we postponed on purpose, which decisions we still argue about.

This is a high-level diary, not a changelog. We will not recite build dates or internal architecture beyond what the docs and the security page already publish - partly for discipline, partly because the interesting part of a beta is the reasoning, not the timestamps. If any of it is useful to someone shipping their own product, the diary has earned its keep twice.

The first thing that existed: the endpoint

Before there was a dashboard, before there was a homepage, there was a URL that accepted a POST. The permanent endpoint - https://sheetlinkforms.com/f/{token}, taking urlencoded, JSON, or multipart - was the first thing that worked, because it is the thing everything else exists to serve.

A form backend is a promise: point your form here and nothing gets lost. Every other surface - the dashboard, the embed, the marketing site - is commentary on that promise. So the earliest work went into the unglamorous contract: parse the content types correctly, enforce the 256KB cap, 303-redirect HTML posts to a thank-you page, answer AJAX with {"ok":true}. The exact behavior is specified on how it works.

Why endpoint-first was the right order

The tempting order is the opposite one: build the beautiful dashboard, demo it, then wire up ingestion. We think that order is backwards for infrastructure. A dashboard bug embarrasses you; an ingestion bug loses someone's lead, and a lost lead is unrecoverable in a way a UI glitch never is.

Building the endpoint first also forced the right early questions - what happens when the destination API is down, when a payload is malformed, when the same client posts fifty times in a minute - while the answers were still cheap to change. The asynchronous delivery worker with retries at 5 minutes, 30 minutes, and 2 hours came out of confronting those questions before any pixel of dashboard existed.

Spam defense as a launch feature

The most consequential scheduling decision of the beta: spam handling shipped with the product, not after it. The standard startup playbook says defer it - spam is a scale problem, and you have no scale. The playbook is wrong for form backends, for a blunt reason: any URL that accepts a POST starts collecting junk almost as soon as it is discoverable. Bots do not wait for your launch post.

So the defenses documented in our spam protection guide - the honeypot, per-form and per-IP rate limits, origin allowlists, content heuristics with a strictness dial, the timing signal from sl.js - were launch scope. A form backend without spam defense is not a smaller version of the product. It is a different, worse product.

The quarantine decision

Within spam defense sat the one decision we debated hardest: what to do with a submission the heuristics distrust. Every instinct from consumer software says drop it silently - users see a clean sheet and credit you for accuracy. We went the other way: suspicious submissions are held in a reviewable quarantine, one click approves and delivers, and only the honeypot - a signal with effectively no honest explanation - marks spam outright.

The reasoning was failure asymmetry: a false positive that gets dropped is a lost lead nobody knows about, while a false positive that gets quarantined costs a click. We have written a whole essay arguing this position; here it is enough to record that it was decided early, on principle, and has survived contact with real traffic.

Eating our own cooking

The homepage waitlist ran on the product - a hosted form posting through sl.js to our own endpoint, landing in a Google Sheet with attribution columns. That decision paid for itself in ways we did not fully anticipate: we rewrote the inline success text because we kept reading it, we watched attribution persistence prove its worth as visitors wandered before signing up, and we reviewed our own quarantine queue, which kept the cost of the never-drop policy vivid to us.

The public version of the same pipeline is the live demo, where the destination sheet is open for anyone to watch rows arrive.

What we deliberately left out

A beta is defined by its omissions as much as its features, so here are ours, plainly. SheetLink sends no notification emails - rows land in your sheet and nothing lands in your inbox, a real limitation for email-centric workflows that we discuss honestly elsewhere on this blog. Multipart posts are accepted but file parts are currently dropped - the fields deliver, the attachments do not. And the email-in address that turns Label: value mail into rows is currently in setup, which is exactly how the docs describe it.

Each omission was a choice to spend beta effort on delivery reliability instead. We would rather ship a short list of behaviors that always hold than a long list that mostly does - and we would rather you learn a limitation from our own pages than from a surprise in production.

Publishing what we can commit to

Early on we adopted a rule: the docs and the security page describe only shipped behavior, in specifics - request formats, redirect codes, retry timing, the formula-injection guard that escapes leading =, +, -, and @ in cell values, the drive.file scope that limits our Google access to sheets you explicitly pick. If something is not ready, the page says so, in the present tense.

This sounds like table stakes. In practice it is a forcing function: writing "currently in setup" on a public page is uncomfortable enough that it keeps the roadmap honest. The security page exists because a form backend asks for real trust and should earn it in writing.

What beta meant here

Concretely: during the beta the product was free, access was by invite through the waitlist, and there were no per-submission fees - a rule that survived the beta and stands permanently in our terms. Signup is now open: a free tier of 100 submissions a month, and Pro at $19/month or $190/year, unmetered - see the pricing page.

Beta also meant the deal ran both ways. Early users got a free product and disproportionate influence over what got built next; we got the thing no amount of internal testing supplies - forms we did not design, on sites we have never seen, submitting data we did not anticipate. Every oddly shaped payload that delivers correctly is a small proof the contract holds.

What the diary says so far

Reading our own entries back, one theme recurs: sequencing is strategy. Endpoint before dashboard. Spam defense before launch. Ledger before notifications. Published limitations before polished claims. None of these orderings were free - each deferred something users legitimately want - but each put the irreversible thing (a lost lead, a broken trust) ahead of the recoverable thing (a missing convenience).

The diary continues. If you want to be in the next chapters rather than reading them, signup is open - start free, no invite required.

FAQ

What shipped first in the SheetLink Forms beta?

The endpoint - a permanent /f/{token} URL accepting urlencoded, JSON, and multipart POSTs - worked before the dashboard existed. We built the ingestion contract first because a lost submission is unrecoverable in a way a UI bug never is.

Why was spam protection part of launch scope?

Because any POST-accepting URL starts attracting bots almost as soon as it is discoverable. Deferring spam defense would have meant early users cleaning junk out of their sheets, so the honeypot, rate limits, heuristics, and quarantine shipped with the product.

What is the quarantine decision?

Suspicious submissions are held for one-click review rather than silently dropped - approving delivers the row. Only the honeypot marks spam outright. The reasoning is failure asymmetry: a dropped false positive is an invisible lost lead; a quarantined one costs a click. See the quarantine glossary entry.

What did the beta deliberately leave out?

No notification emails (rows only), file parts of multipart posts are dropped (fields still deliver), and the email-in address is currently in setup. Each omission bought effort for delivery reliability, and each is stated in the docs rather than glossed over.

Was the waitlist really running on the product?

Yes - the homepage waitlist posted through sl.js to our own endpoint and landed in a Google Sheet with attribution columns. The public demo is the same pipeline with an openly viewable destination sheet.

How does delivery reliability actually work?

Submissions deliver asynchronously, direct to the Google Sheets API or Microsoft Graph with no middleman, with automatic retries at 5 minutes, 30 minutes, and 2 hours and a per-submission delivery log in the dashboard.

What does pricing look like now?

The beta is over and signup is open. The free tier covers 100 submissions a month, 3 forms, one connected destination, and 30 days of history - no card, no invite. Pro is $19/month or $190/year (2 months free), unmetered, with 24 months of history and no per-submission fees, ever. Details on the pricing page.

How do I get an account?

Start free - signup is open, with no invite and no card required. The free tier covers 100 submissions a month, 3 forms, and one connected destination.

Be in the next chapter

Signup is open and the free tier needs no card - 100 submissions a month, on the product itself.

Start freeSee the live demo

Email notifications vs spreadsheet rowsThe quarantine queue: a product argument