How HurricaneWise is put together
The pipeline that runs every minute is on a $7.60 Hetzner box, the public site is on Vercel, and my estimate for the whole bill is about $15 a month.
The parts
I've tried hard to keep this small. One server polls the official NHC and NOAA feeds every 60 seconds, asks Claude to explain the models when an advisory changes, and stores the result. Visitors read that from a static site on Vercel with a thin read-only API behind it. Grafana checks the public site from outside and takes in the server's logs and metrics, and GitHub Actions runs the tests and cuts releases.
| 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 | interprets eleven forecast models and runs repair sessions |
| Hetzner | one fixed-price box running six recurring jobs |
| Grafana Cloud | metrics, logs, and probes hitting the site from outside |
| GitHub Actions | lint, types, tests, end-to-end, and deploy on a version tag |
Server or managed platform
The pipeline wakes up every 60 seconds whether anyone visits or not, so I put it on the Hetzner box, which costs $7.60 a month no matter the load. On a metered platform those wake-ups would get billed every minute of the month. Visitor traffic comes in spikes, basically a lot of people checking one storm at once, and Vercel absorbs that without me doing anything.
At first I ran the pipeline schedule in GitHub Actions on a self-hosted runner. The five-minute trigger often fired only six to eight times a day, so the site could serve an advisory hours past its expected age and my monitoring kept filing incidents against the pipeline. I treated it as a data problem for weeks. Then I moved the timer onto the server that does the work, and the runs stopped going missing.
There's a test now that rejects recurring pipeline schedules on hosted runners, and the GitHub workflows are manual-only.
What the server runs
The server is a plain Ubuntu machine at Hetzner, about $7.60 a month ($6.99 for the box and $0.60 for the public IP). It runs six recurring jobs under the operating system's own supervisor, which restarts them when they fall over and keeps their logs. The pipeline polls the feed every minute but only wakes the core database when an advisory changes, roughly eight to fifteen times a day plus a few discussion updates. Quota projection and the daily SRE check don't run here. Those run from my own machine.
| 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 slow-moving services, opens and closes incidents |
| Grafana sync | 15 min | meant to copy the external check results into the local health history; not producing data yet |
| incident resolver | 5 min | sweeps for detected failures except incidents affecting itself |
| telemetry agent | always | ships host metrics and job logs to Grafana |
| OS-posture timer | 15 min | emits reboot, update and failed-unit metrics from what the operating system reports |
How often the checks run
Four Grafana checks run from outside my infrastructure, one every 60 seconds and the other three every 3 minutes. Only one runs every minute because the external checks are what use up Grafana's free allowance. The server's internal loop watches slow-moving things with alarm thresholds measured in hours, so every 15 minutes is enough for it.
Metrics, logs and the outside checks
The server ships metrics and logs to Grafana, so I can see what the machine and the jobs are doing and what the jobs wrote. I didn't set up distributed tracing and so far nothing has needed it.
Four Grafana checks run outside my infrastructure. The server's metrics can look normal while the public site is unavailable, so when internal telemetry and the outside view disagree, I trust the outside view. One internal probe used to take its own running loop as proof the pipeline was healthy and never looked at advisory age; now the site and that probe share one freshness evaluator.
There are nineteen health checks in all, and six of them check the other checks using what the operating system reports about each scheduled job. If the collector that gathers those reports breaks, those six show no result at all. An incident opens on the second bad check in a row.
No visitor text reaches the model
Nothing a visitor types reaches the AI model. The server writes the storm reads ahead of time from official NHC and NOAA data, and a page view doesn't start a model call.
No visitor text reaches the model, so visitors cannot supply a prompt through the public site. Prompt injection is first in OWASP's 2025 LLM Top 10. The pipeline and repair sessions still have spending caps, since scheduled work burns tokens with or without visitors.
There are no user accounts and no personalization. "Storms near you" uses an approximate location worked out at the edge. The only personal data kept is what the feedback form records to stop abuse, the submitter's IP and browser string.
I haven't added accounts or personalization. Holding a person's data would bring consent and retention responsibilities that I am not taking on yet. New features are outside what the repair agent is told it may do.
The feedback form uses Turnstile, verified on the server. The API has per-IP rate limits and burst detection. There are two privileged routes and both need a token. Secrets live in a vault and get synced into the deploy, never into the repository. The internal operations dashboard sits behind a zero-trust check (Cloudflare Access) that refuses entry if the check itself fails, and that's where the two kill switches are, one for the resolver and one for its permission to deploy.
Free tiers
The free tiers run on the same Vercel network, the same Postgres engine at Neon and the same Cloudflare storage as the paid plans, just with lower volume limits. Grafana's is the tightest. It allows 100,000 synthetic checks a month and the monitoring is designed to use about 86% of them.
Neon, Cloudflare R2, Vercel, Grafana Cloud and GitHub Actions show up as $0 on free tiers. I run a cost snapshot script that pulls usage from every provider and lays out the bill and quota headroom line by line.
Rules every change follows
Most commits here come from AI agents, and they get held to the same few rules as mine.
Production runs on Postgres and local tests on SQLite, through the same interface. Every change ships with a test next to the code it covers (about 2,200 tests by mid-October 2026, including browser checks on desktop and mobile). Infrastructure is version-controlled config, and prompts and config files each have one fixed home in the repo.
On July 26, 2026 I wrote those rules into a versioned constitution (v1.0.0) and ratified it after an adversarial review. It also requires every monitor to be registered in a manifest before it exists. CI checks the manifest, so an unregistered probe fails the build.