I got tired of pasting the same 50 numbers into an LLM chat every morning, so I replaced myself with a pipeline.
The short version: it watches a set of values I care about, decides which ones need my attention, and tells me before I’d have caught it myself. What it tracks and why is its own post. This one is about how it’s built.
It runs daily. It ingests data from public APIs, classifies it through a deterministic rule engine, pushes an alert to my phone when a threshold crossing is detected, renders a public dashboard, and serves a login-gated private report. And every piece of infrastructure it runs on is a free tier.
Not “cheap.” Zero. The only thing I pay for is the LLM subscription I already had for other things.
This post is the tour: what each component does, the free-tier gotchas I hit in real operation, and something I didn’t expect, that having no budget forced a better architecture than a paid stack would have.
The stack
GitHub Actions. Runs the daily ingest job: fetch from the source APIs, upsert into the database. Finishes in seconds, well inside the free minutes on a public runner. More on “daily” shortly, it’s doing more work than it sounds.
Supabase. Real Postgres on the free tier. Row-level security decides exactly what the anonymous public role can read, so the dashboard queries it directly with no backend gatekeeping every request.
A deterministic rule engine. Plain code that classifies every reading after ingestion: good band or bad, threshold crossed or not, streak confirmed or not. No model, no drift. It’s the design choice I’d defend hardest here, more on why in the next post.
Cloudflare Pages. Static hosting for the public dashboard on a global CDN, deployed on every push. Free tier, no cold starts, nothing to patch.
Pages Functions + Cloudflare Access. The most underrated free combo in the stack. A Pages Function renders the private report server-side, so the database’s privileged key stays in an edge environment variable and never reaches the browser. Access adds email one-time-PIN login in front of it. Gated page, no exposed credentials, no cost.
ntfy.sh. Sends a push notification to my phone on a threshold crossing: one HTTP POST, no Twilio account, no custom app, no bill. Five minutes to set up, then you wonder how you shipped side projects without it.
A local MCP server. Reads the data that should never leave my machine, from local files on hardware I already own, and joins it with the public cloud data at read time, inside the LLM chat where I read the day’s report. The cloud never sees it. More on why this split exists below.
Gotcha #1: free cron is not punctual cron
GitHub Actions’ scheduled workflows are queued on shared infrastructure, and the docs are upfront that they can be delayed. In my real run history, the daily job has fired up to an hour late.
If you chain downstream steps on fixed time offsets (say, ingestion runs at 9, so the report generates at 9:30), a late cron silently hands your report yesterday’s data, and nothing errors. The fix is to stop trusting the schedule and start checking the data: every ingestion run stamps a freshness marker, and every downstream consumer checks that marker before doing anything. If the data isn’t fresh, the consumer refuses to run and says so, loudly.
That guardrail has already caught real failures, not just late crons but a silently failing workflow after a dependency update. A paid cron with an SLA would have hidden the fragility until the day it didn’t.
Gotcha #2: no budget for middleware is a security feature
The system handles two categories of data: public-shareable and genuinely private. With a budget, the lazy design is one hosted database for everything plus an auth layer, and now your private data’s safety depends on that auth layer being right forever.
With no budget for hosted middleware, I was forced into a cleaner model: private data never gets uploaded anywhere, period. It lives in local files on my machine, git-ignored, read only by the local trusted layer. The cloud database holds only what the public dashboard is allowed to show anyway, so if the anon key leaked tomorrow, the blast radius is data that was already public.
The rule is enforced in code, not something I have to remember: the write paths that persist to the cloud validate that no private-category values are present in the payload, and reject the write if one slips in. The constraint produced a threat model I’d actually defend, and I backed into it because I was too cheap to pay for a middle tier.
What this adds up to
What separates a weekend hack from a reliable system is design, not spend: freshness checks instead of schedule trust, deterministic classification instead of vibes, a hard boundary between public and private data instead of an auth layer and good intentions. Every one of those decisions was free. Several of them were caused by free.
The harder problem in this system wasn’t infrastructure at all. It was figuring out where an LLM breaks down when you put it in a daily loop and ask it for consistent judgment. That one gets its own post.
Questions or war stories about your own free-tier production systems? Find me on LinkedIn. I’d genuinely like to hear what you’re running.