Real-time radar, predictive nowcasting with calibrated confidence, and geospatial queries — for applications that need to understand and react to weather in real time. Every forecast scored against reality, in public.
Live demo · Map · Storm Replay · Calibration · Docs · Swagger
The API is live at https://api.aeroza.app. These run right now, from your terminal:
# What is the radar reflectivity over downtown Houston, this minute?
curl -s "https://api.aeroza.app/v1/mrms/grids/sample?lat=29.76&lng=-95.37"{
"type": "MrmsGridSample",
"product": "MergedReflectivityComposite",
"level": "00.50",
"validAt": "2026-07-25T16:44:38Z",
"value": 14.199999809265137,
"requestedLatitude": 29.76,
"requestedLongitude": -95.37,
"matchedLatitude": 29.75500000000422,
"matchedLongitude": -95.36500098959021,
"toleranceDeg": 0.05
}A real response, minus the fileKey and variable fields. You asked for a point; you get back the grid cell that actually answered, how far off it was, and when it was valid.
# Is anything above 40 dBZ inside this polygon? (lng,lat,lng,lat,… — GeoJSON order)
curl -s "https://api.aeroza.app/v1/mrms/grids/polygon\
?polygon=-95.7,29.5,-95.0,29.5,-95.0,30.0,-95.7,30.0&reducer=count_ge&threshold=40"
# Every active NWS alert, as a GeoJSON FeatureCollection you can drop straight into a map
curl -s "https://api.aeroza.app/v1/alerts?limit=5"
# New alerts, streamed as they are issued (Server-Sent Events)
curl -N "https://api.aeroza.app/v1/alerts/stream"
# How good were the last 24h of forecasts? (this is the whole point)
curl -s "https://api.aeroza.app/v1/calibration"| You want to know… | Call |
|---|---|
| Radar value at a point | GET /v1/mrms/grids/sample?lat=&lng= |
| Max / mean / min / count over a region | GET /v1/mrms/grids/polygon?polygon=&reducer= |
| Active watches & warnings (GeoJSON) | GET /v1/alerts |
| Alerts as they happen | GET /v1/alerts/stream (SSE) |
| Radar as map tiles | GET /v1/mrms/tiles/{z}/{x}/{y}.png |
| Predicted grids, 10 / 30 / 60 min out | GET /v1/nowcasts |
| Whether those forecasts were any good | GET /v1/calibration |
| Surface observations | GET /v1/metar, GET /v1/metar/{station}/latest |
| System freshness | GET /v1/stats, GET /health |
Coverage is CONUS-first, because that's where MRMS and NWS coverage is dense. Times are ISO-8601 UTC; the wire is
camelCase;bbox/polygonuselng,lat(GeoJSON),pointuseslat,lng.
|
Live map — MRMS reflectivity tiles under active NWS polygons, with a 1-hour loop scrubber. The tiles come from the same public
|
Storm Replay — autoplay through your local archive, or through curated historical events (Houston derecho, Mayfield quad-state tornado, the 2011 Super Outbreak) with commentary.
|
|
Calibration — every nowcast is scored against the observation grid that lands after it. MAE / bias / RMSE, POD / FAR / CSI, Brier / CRPS, sample-weighted by cell count. Published, not claimed.
|
Dev console — a live test harness over the API: SSE stream, alert list, MRMS catalog, point sampler, webhook and alert-rule CRUD. Every panel is driven through the
|
Requires Docker, Node 20+, and uv.
make startThat runs make doctor (preflight), make bootstrap (writes .env with a real signing salt, syncs Python deps, starts Postgres / Redis / NATS, applies migrations), then brings up the full stack under honcho: the API, the console, the alerts / MRMS / METAR ingest workers, and the webhook dispatcher. Ctrl+C tears it all down. Re-running is idempotent.
| Service | Where it lands |
|---|---|
| API | http://localhost:8000 — health at /health, Swagger at /docs |
| Console | http://localhost:3000 — landing, /map, /replay, /calibration, /console, /docs |
Your dashboard fills itself in. On first make start, scripts/seed-historical.sh backfills ~3 hours of MRMS in the background so you aren't staring at empty panels while live ingest catches up. Tail it with tail -f .seed.log, or run it explicitly with make seed. Idempotent — it short-circuits once the catalog has data.
With the [grib] extra installed (make extras-grib), make start also runs the GRIB → Zarr materialiser, the lagged-ensemble nowcaster, and the verifier, so radar replay and /calibration light up out of the box. make stop brings the Docker layer down.
Driving the pieces by hand
make bootstrap # one-time setup (idempotent)
make dev # FastAPI on :8000 — terminal 1
make web-dev # Next console on :3000 — terminal 2
make ingest-alerts # NWS active alerts → /v1/alerts*
make ingest-mrms # MRMS file catalog → /v1/mrms/files
make ingest-metar # METAR observations → /v1/metar
make materialise-mrms # MRMS GRIB2 → Zarr → /v1/mrms/grids* (needs [grib])
make nowcast-persistence # persistence baseline
make nowcast-lagged-ensemble # probabilistic baseline (Brier/CRPS) — no extras needed
make seed # backfill ~3h of history (idempotent)
make check # lint + format check + mypy + unit testsEvery entry is also a top-level aeroza-* console script. make help lists all targets.
Optional extras: [grib] and [nowcast]
make start installs only the core extras the live stack needs. Two heavy, system-dependency-bearing ones stay opt-in.
Why
make extras-*and not plainuv sync --extra X?uv sync --extra Xreplaces the installed extra-set rather than adding to it — runninguv sync --extra gribon a working stack silently uninstalls db / cache / stream / ingest / verify and breaks everything else. Themake extras-*targets re-list the bootstrap extras so adding one stays additive.
[grib] — cfgrib, for decoding MRMS GRIB2. Required by aeroza-materialise-mrms, which fast-fails at startup with an install hint if it's missing.
# macOS
brew install eccodes && make extras-grib
# Debian/Ubuntu
sudo apt-get install -y libeccodes-dev && make extras-grib[nowcast] — pysteps, for the optical-flow forecaster. Opt in with aeroza-nowcast-mrms --algorithm pysteps; without it the worker keeps running the persistence baseline.
# Linux: pysteps' setup.py just works.
make extras-nowcast
# macOS: pysteps' setup.py passes raw -fopenmp, which Apple clang rejects.
brew install libomp
CFLAGS="-Xpreprocessor -fopenmp -I$(brew --prefix libomp)/include" \
LDFLAGS="-L$(brew --prefix libomp)/lib -lomp" \
make extras-nowcastFor both at once, make install runs uv sync --all-extras — fine when eccodes and libomp are already in place.
@aeroza/sdk pins every wire shape and is what the dev console drives every panel through — so the contract is dogfooded on every change. In-tree today (workspace dep), not yet published to npm.
import { AerozaClient } from "@aeroza/sdk";
const client = new AerozaClient({ apiBase: "https://api.aeroza.app" });
// Point sample
const sample = await client.sampleGrid({ lat: 29.76, lng: -95.37 });
// → { type: "MrmsGridSample", value: 14.2, matchedLatitude: 29.755, … }
// "How many cells in this region are at or above 40 dBZ?"
const region = await client.reduceGridOverPolygon({
polygon: "-95.7,29.5,-95.0,29.5,-95.0,30.0,-95.7,30.0",
reducer: "count_ge",
threshold: 40,
});
// Live alerts — the SDK builds the URL, you own reconnection
const source = new EventSource(client.alertsStreamUrl());
source.addEventListener("alert", (e) => {
const alert = JSON.parse((e as MessageEvent).data);
console.log(alert.event, alert.severity);
});Non-2xx responses throw AerozaApiError carrying status and FastAPI's detail.
Subscribe a URL, then attach rules that fire on the data itself — not just on "an alert was issued".
# 1. A subscription. The response carries the signing secret exactly once.
curl -s -X POST http://localhost:8000/v1/webhooks \
-H 'content-type: application/json' \
-d '{"url":"https://example.com/hooks/aeroza","events":["aeroza.mrms.grids.new"]}'
# 2. A rule: tell me when reflectivity over my warehouse crosses 45 dBZ.
curl -s -X POST http://localhost:8000/v1/alert-rules \
-H 'content-type: application/json' \
-d '{
"subscriptionId": "<id from step 1>",
"name": "Warehouse hail watch",
"config": {
"type": "point", "lat": 29.76, "lng": -95.37,
"predicate": { "op": ">=", "threshold": 45 }
}
}'Polygon rules take the same shape with "type": "polygon", a polygon string, and a reducer (max / mean / min / count_ge). A rule fires on the not-firing → firing transition and delivers aeroza.alert_rules.fired to its bound subscription — bound implies subscribed, so that event doesn't need to be in the subscription's events array. The subscribable stream events are aeroza.alerts.nws.new, aeroza.mrms.files.new, aeroza.mrms.grids.new, and aeroza.nowcast.grids.new.
Every delivery is signed Stripe-style — Aeroza-Timestamp plus Aeroza-Signature: v1=<hex>, an HMAC-SHA256 over timestamp + "." + payload, with a 5-minute replay window. Delivery attempts are auditable at GET /v1/webhooks/{id}/deliveries.
API keys (aza_live_…) are bearer tokens; GET /v1/me introspects one. Auth is opt-in per deployment via AEROZA_AUTH_REQUIRED, so local development stays frictionless.
curl -s https://api.aeroza.app/v1/me -H "Authorization: Bearer aza_live_…"A modular monolith (FastAPI) with the long-running work extracted into workers.
flowchart LR
subgraph sources[Public data]
NWS[NWS alerts API]
MRMS[NOAA MRMS on S3]
METAR[METAR / AWC]
end
subgraph workers[Ingest workers]
ING[alerts · mrms · metar]
MAT[GRIB2 to Zarr materialiser]
NOW[nowcasters]
VER[verifier]
DISP[webhook dispatcher]
end
subgraph storage[Storage]
PG[(Postgres + PostGIS)]
S3[(S3 / R2 · Zarr)]
REDIS[(Redis)]
end
NWS --> ING
MRMS --> ING
METAR --> ING
ING --> PG
ING --> NATS{{NATS JetStream}}
NATS --> MAT --> S3
MAT --> NOW --> S3
NOW --> VER --> PG
NATS --> DISP --> HOOK[your endpoint]
PG --> API[FastAPI · /v1]
S3 --> API
REDIS --> API
API --> SDK["@aeroza/sdk"]
API --> WEB[Next.js console]
SDK --> WEB
aeroza/
cli/ Long-running workers + one-shot CLIs (the aeroza-* scripts).
ingest/ NWS alerts + MRMS reflectivity + METAR observations.
query/ REST read-side — alerts, MRMS files/grids/tiles, METAR,
nowcasts, calibration, stats. Split per domain under query/v1/.
nowcast/ Forecaster Protocol + persistence, pySTEPS Lucas–Kanade,
and lagged-ensemble forecasters.
verify/ Continuous verification + sample-weighted aggregates:
MAE/bias/RMSE, POD/FAR/CSI, Brier/CRPS.
webhooks/ Signed delivery, subscription CRUD, alert-rule DSL, dispatcher.
tiles/ XYZ raster tiles for MRMS reflectivity.
auth/ Bearer-token API keys (aza_live_*), opt-in.
push/ APNs device registration + severe-weather push.
stream/ NATS publishers/subscribers + the SSE gateway.
admin/ Dev-console-only seed-event endpoints (gated).
shared/ DB session helpers, common schemas, HTTP client.
25 public /v1 paths (32 operations), plus /health. Postgres 16 + PostGIS, Redis, and S3-compatible object storage for Zarr; NATS JetStream for streaming. The sdk-ts/ client pins the wire shapes.
make check # lint + format check + mypy + unit tests — what CI runs
make test # unit tests (DB-dependent tests auto-skip)
make test-integration # integration tests (needs `make up`)
make test-cov # coverage report872 tests, 543 of which run without infrastructure. CI runs ruff, ruff-format, mypy, and the unit suite with a coverage floor on every push and PR. CONTRIBUTING.md covers the branch / PR / merge flow.
The README screenshots above are generated, not hand-cropped — refresh them with:
node scripts/capture-screenshots.mjsPhases 0–6f shipped. Live ingest (NWS alerts, MRMS reflectivity, METAR), queryable point / polygon / tile reads, three nowcasting algorithms (persistence baseline, pySTEPS Lucas–Kanade, lagged-ensemble), continuous verification across continuous / categorical / probabilistic skill scores, signed webhook delivery with the point + polygon alert-rule DSL, opt-in bearer-token auth, APNs severe-weather push, and a six-route web surface driven entirely through the TypeScript SDK.
Next: a STEPS ensemble forecaster with real stochastic perturbations, reliability diagrams on /calibration, per-key rate limiting, and more ingest sources (NEXRAD Level II, HRRR / NBM). Details in docs/ROADMAP.md.
| Doc | What's in it |
|---|---|
| Roadmap | docs/ROADMAP.md — phase plan, shipped vs. next |
| Changelog | CHANGELOG.md — release-style notes per phase |
| Contributing | CONTRIBUTING.md — branch / PR / merge flow |
| Deploy | docs/DEPLOY-RAILWAY.md — Railway + Supabase + Vercel |
| SDK | sdk-ts/README.md — @aeroza/sdk reference |
Radar and alert data courtesy of NOAA/NSSL MRMS and the National Weather Service. Basemap by OpenFreeMap / OpenMapTiles, data © OpenStreetMap contributors.
MIT — see LICENSE.



