The architecture behind analytics you actually own
One serverless function, one small database, three kinds of event. Here is how it scales to a hundred million events for tens of dollars, and how we just locked the contact box against bots.
The architecture of analytics we fully own, and the anti-bot stack we shipped this week.
We built our own analytics, and we mean the whole thing: the code that catches an event, the database it lands in, and the dashboard we read it from. It runs in production today at metrics.gattyworks.com, measuring our own launches and a client site. This week we also shipped the security that keeps bots out of it. Here is the full design, the numbers we think it can hold, and the honest edges.
The entire system fits in one picture
There is one serverless function running at the edge, and one small SQL database. That is the system. No origin server, no cron, no queue, no message bus, no cookie, and no third party in the middle holding your data. The public internet can reach exactly one thing: a single write endpoint.
A visitor taps something. A tiny script, about a kilobyte, posts one event to the function, which writes one row and returns. The dashboard is a separate door: an operator signs in through single sign-on, behind a secret path, and only then can read anything back. The side that writes and the side that reads never share a key, and the write path exposes no read route.
Three kinds of event are enough for any funnel
We assumed we would need a sprawling taxonomy of event types. We did not. The whole system speaks three words. A click means someone paid attention. An answer means they picked an option. A contact means they handed over an email or a phone number. Every funnel we have wanted to build is a sequence of those three, and nothing else.
Because the vocabulary is fixed and tiny, the server never changes per use case. What changes is a small piece of config that says which steps to show. An email grab is one contact step. A qualify-then-call flow is a click, two questions, then a contact. Same engine, different config.
{
"steps": [
{ "kind": "question", "id": "building", "prompt": "What are you building?" },
{ "kind": "contact", "fields": ["email", "phone"] }
]
}How much can it hold
Here is the trick that makes the numbers work. We only record intent: a click, an answer, a contact. We never fire on pageviews, scrolls, or mouse moves. For the same human traffic that is ten to a hundred times fewer writes than typical analytics. So the workload is already tiny before any platform limit shows up.
On the free tier, roughly a hundred thousand of these events a day, about three million a month, costs nothing at all. That covers most real sites, launches, and waitlists outright. Past that, the paid math stays gentle, and these are back-of-the-envelope figures, not a benchmark:
Still on the back of the envelope: one database on the tier we run should hold tens of millions of lifetime events. When a single tenant outgrows that, you do not rewrite anything. You give each tenant its own database and one function binds them all, so capacity grows in a straight line.
The one box that bots care about
Clicks and answers are cheap noise. The place spam actually hurts is the contact box, where a script could mint a thousand fake leads in a loop. This week we shipped three layers to stop that, and left clicks and answers frictionless.
- An invisible bot challenge on submit. Real visitors practically never see a checkbox; a token is minted silently and verified on the server. If verification fails, the contact is refused.
- A per-browser cap. A best-effort fingerprint lets us accept two submissions from a browser, the first plus one edit to fix a typo, and reject the third.
- The lock lives on the server, not the browser. The count is enforced in the database, so clearing local storage does not reset it. The browser side is only there to make the experience nice.
How this compares to what you already pay
Honest framing. Prices move; the structural differences are the durable part.
- Mainstream free analytics: genuinely free, but the data lives on someone else's servers, high volume gets sampled, you carry the consent-banner overhead, and storing personal contacts is against the terms. Here you own the raw rows and nothing is sampled.
- Privacy-first hosted analytics: pleasant and lightweight, but priced per pageview and still on the vendor's box. At ten million pageviews a month you are into the low hundreds of dollars, and it is not your database.
- Product analytics suites: powerful and heavy, a full SDK on the client and your data on their cloud. Great for deep product questions, overkill for did-this-launch-land.
- Self-hosted analytics: the closest in spirit, you own your data, but you are running a heavy data stack yourself. We trade breadth for something one person can hold in their head.
Where we would change our minds
The edges, stated plainly. We would reach for something bigger if a tenant needed full-text or high-cardinality analytical queries a small SQL database cannot serve, if a single tenant blew past the per-database ceiling and could not tolerate rollups, or if event delivery had to be guaranteed rather than best-effort. None of those are close for the traffic we and our clients generate, and each is an addition, not a rewrite.
The design is deliberately unexciting. Own the data, keep the system small enough to reason about, let the free tier carry most sites at no cost, and put the security only where it matters. If you want this wired into your own launch, that is something we do.