Felipe Lachowski
← The series Episode 3 of 7

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.

October 2026

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.

ServiceJob
Vercelthe static site and a read-only API, served from the global edge
Neonserverless Postgres that scales to zero when nothing is happening
Cloudflare R2generated graphics plus a small freshness signal, no egress fees
Anthropicinterprets eleven forecast models and runs repair sessions
Hetznerone fixed-price box running six recurring jobs
Grafana Cloudmetrics, logs, and probes hitting the site from outside
GitHub Actionslint, 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.

JobCadenceWhat it does
data pipeline60spolls the NHC, wakes the billed database only when an advisory has actually changed
health loop15 minprobes slow-moving services, opens and closes incidents
Grafana sync15 minmeant to copy the external check results into the local health history; not producing data yet
incident resolver5 minsweeps for detected failures except incidents affecting itself
telemetry agentalwaysships host metrics and job logs to Grafana
OS-posture timer15 minemits 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.