Seven services and one rule about where work lives
Choosing the services took an afternoon. Deciding which work belonged on which of them is the decision that kept the bill near zero, kept the pipeline punctual, and made the security model a property of the architecture rather than a feature bolted on at the end.
The whole system, in one paragraph
The system is small and I have worked to keep it that way. Official data comes in from the National Hurricane Center and NOAA. A small server picks it up, asks a model to write the interpretation, and stores the result. A static site and a thin read-only API serve it. Grafana watches all of it and GitHub Actions ships the code that does the work. That is the whole thing.
| Service | Job |
|---|---|
| Vercel | the static site and a read-only API, served from the global edge |
| Neon | serverless Postgres that scales to zero when nothing is happening |
| Cloudflare R2 | generated graphics plus a small freshness signal, no egress fees |
| Anthropic | the one job no rule could write: turning fifteen disagreeing models into a sentence |
| Hetzner | one fixed-price box running everything recurring, around the clock |
| Grafana Cloud | metrics, logs, traces, and probes hitting the site from outside |
| GitHub Actions | lint, types, tests, end-to-end, and deploy on a version tag |
The rule
Every placement decision in this system came down to one question. Is this work bursty and customer-facing, or steady and cheap? Bursty, customer-facing work goes to managed platforms, because absorbing spikes and staying up without me is the entire reason those platforms exist. Steady work that runs every minute goes to a fixed-price box, because metered platforms punish residents and a machine you rent by the month does not care.
That sounds obvious written down. It cost me a real outage to learn properly. I originally scheduled the data pipeline on GitHub Actions, running every five minutes on my own self-hosted runner, which felt like the best of both. It fired six to eight times a day. The scheduling trigger is best-effort, and mine was not the priority. Moving the clock onto the machine that does the work fixed a class of incident I had been treating as a data problem for weeks.
A rule in the config now refuses to schedule any recurring pipeline on a hosted runner. It is enforced by a test, not by my memory.
Five jobs on one small box
The server is a plain Ubuntu machine costing $7.60 a month, which is $6.99 for the box and $0.60 for its public address. There is nothing exotic on it. Five small single-purpose services managed by the operating system's own supervisor, which restarts them when they fall over and keeps their logs.
| Job | Cadence | What it does |
|---|---|---|
| data pipeline | 60s | polls the NHC, wakes the billed database only when an advisory has actually changed |
| health loop | 15 min | probes the slow-moving services, opens and closes incidents |
| incident resolver | 5 min | sweeps for the failures it is allowed to fix |
| metrics and logs | always | ships host and job telemetry to Grafana |
| maintenance pass | daily | projects quota burn and reviews the server’s posture, results pushed into Grafana |
Unequal cadences are a cost decision
The cadences are deliberately unequal and that is a cost decision as much as a reliability one. The checks a visitor would actually feel run every sixty seconds, from outside my infrastructure, on Grafana's machines. The internal loop watches slow-moving things whose alarm thresholds are measured in hours, so it runs every fifteen minutes. Probing them every minute bought nothing except a database that never got to sleep.
Watching from both sides, and which side wins
An agent on the server ships three streams into Grafana: metrics about the machine and the jobs, the logs those jobs produce, and traces of work moving through the pipeline. Together they answer the three questions you have during an incident. Is it healthy, what did it say, and where did the time go.
Internal telemetry has one permanent weakness. It can only tell you that the server thinks it is fine, and the thing that is broken might be the thing doing the thinking. So separate probes load the public site from outside every sixty seconds, the way a person would. When the server's own metrics look green and the external probe cannot reach the site, the external probe wins. It is closer to the truth a visitor is living in.
Eighteen checks watch this system. Six of them exist only to watch the machinery that does the watching, and they follow a rule earned the hard way: if the collector gathering the facts is broken, report nothing rather than guess. A monitor that invents an outage is only marginally better than one that hides a real one.
Security is what the system refuses to do
The strongest security property in HurricaneWise is an absence. No text a visitor types ever reaches the model. Every storm interpretation, every basin summary, every read on the page was generated ahead of time on the server, from official NHC and NOAA feeds. The website only ever serves something that was already written.
That single decision removes the classic prompt-injection surface, because there is no user-supplied prompt to inject into. It doubles as cost control, because a visitor cannot trigger a paid model call by reloading the page, and a read that already exists costs the same to serve whether one person opens it or ten thousand do. Spending caps sit behind all of it for the failure modes I have not thought of yet.
The second absence is that there is nobody to personalize. No accounts, no logins, no stored profiles. The "storms near you" feature works from an approximate location resolved at the edge, not from anything I keep. There is almost no private data to leak and no authentication system to breach.
I want to be honest that this is a limit as much as a virtue. Personalization is the obvious next chapter if this ever grows, and it arrives carrying accounts, consent, retention, and the full weight of holding someone's data. Choosing not to carry that yet is a stage in the product's life, not a permanent moral position.
Everything else is ordinary discipline made cheap by the decisions above. The feedback form is gated by a server-verified human check. The API has per-address rate limiting and burst detection. The one privileged route is token-gated. Secrets live in a vault and sync into the deploy, never into the repository. The internal operations dashboard sits behind a zero-trust door where every request proves who it is, every time, and if the check cannot complete, the door stays shut.
The free tier is a volume limit, not a quality limit
Free tiers get a worse reputation than they deserve, and the misunderstanding is specific. The reliability of a managed platform does not change when you stop paying. Vercel serves from the same global network. Neon runs the same Postgres engine with the same durability. Cloudflare answers from the same edge. What the free tier caps is volume, and for a single-author product those ceilings sit far above what it uses.
So I did not build a cheap imitation of a reliable system. I rented small slices of genuinely reliable systems and arranged the work so the slices are enough. None of that is something one person could hand-build at the same quality, and none of it is worse because I am not paying.
The spine underneath
A model will happily write code that works once. Code that still works a year later, that someone can change without fear, came from a short list I refuse to bend, enforced on every commit.
One source of truth for anything that matters, which is why freshness is computed by exactly one function that both the site and the server loop call. Small, swappable pieces, which is why the same database interface is satisfied by Postgres in production and SQLite on my laptop. A test with every change, co-located next to the thing it tests, currently about 1,880 of them. Infrastructure in version-controlled config, so nothing important survives only in a dashboard someone clicked once. Configuration and prompts in one governed place each. Secrets in a vault.
None of those are the model's instincts. They are mine, and holding the line on the four hundredth commit is the actual job. This summer they stopped living in my head and became a versioned constitution for the project, ratified after an adversarial review. The principles did not change. What changed is that agents write most of this code now, and a rule that lives only inside a person cannot bind an agent.