The technical reference for the destination alerts system: official-source ingestion, the three qualifying gates, deduplication and the expiry state machine, surfacing rules, and the shadow-trial machinery new sources must pass. For engineers and journalists. Looking for the plain-English version? How Alerts Work →
The pipeline polls each official feed on a fixed cycle with conditional requests — a feed that hasn’t changed costs one cheap check and is skipped. Each source has its own fetcher (alerts/sources/) that normalizes whatever the feed speaks — GeoJSON, CAP, Atom, agency APIs — into one common event shape.
Every report also lands in an append-only observation ledger: one row per source per sighting, the audit trail of which source said what, when. An alert can always answer “who reported this, and how recently?”
Per-source failures are isolated — one broken feed never takes down the cycle — and a feed that fails repeatedly is flagged unhealthy on an operator dashboard rather than failing silently.
{
"sourceKey": "gdacs",
"sourceEventId": "TC-1029-2",
"category": "cyclone",
"severity": "severe", // band, already mapped
"title": "Tropical Cyclone — Yucatán",
"countryCodes": ["MX"],
"lat": 21.2, "lng": -86.8,
"occurredAt": "2026-06-11T09:40:00Z",
"sourceUrl": "https://www.gdacs.org/…"
}A normalized event passes three gates (alerts/gating.ts) or it stays in the ledger unpublished. Gate one: the category must be on the qualifying list at all — a hazard type absent from the table below never qualifies, however severe. Gate two:the severity band must clear that category’s floor. Gate three: the event must map to at least one destination in our catalog — by country code, by region rollup, or, for events that arrive as a bare coordinate, by a severity-scaled radius: Minor 50 km · Moderate 150 km · Severe 300 km · Extreme 600 km. Bigger events reach further.
Advisory changes are the deliberate exception to the floor: every advisory level change qualifies, because the jump itself is the signal — a country moving from level 2 to level 3 is news even though a standing level 3 is not.
The thresholds are operator-tunable configuration, and the table below is rendered from that live configuration — the published numbers cannot drift from the enforcing code.
| Category | Minimum severity |
|---|---|
advisory | Minor |
cyclone | Severe |
earthquake | Moderate |
flood | Severe |
health | Severe |
severe-weather | Severe |
tsunami | Severe |
volcano | Severe |
wildfire | Severe |
Every alert has a deterministic dedupe key — for hazards, the source plus the source’s own event id (gdacs:TC-1029-2); for advisories, a synthesized key that folds in the advisory level and a recurrence counter. A feed that reports the same event fifty times produces one alert, updated in place: severity, title, and coordinates refresh; the alert’s identity and history don’t.
Lifecycle is a one-way state machine: active → resolved, retracted, or expired — and the terminal states are absorbing. A resolved alert never silently reactivates; a genuinely new flare-up gets a new identity and a fresh paper trail.
Expiry is the backstop for silent feeds: every fresh report extends the clock from the report’s own timestamp, and when reports stop, the window runs out and the alert expires on its own. A stale banner cannot outlive its source’s silence.
| Category | Window | Why this window |
|---|---|---|
advisory | 90 days | standing advisories change slowly |
cyclone | 10 days | multi-day track plus the immediate aftermath |
earthquake | 3 days | covers the immediate aftershock window |
flood | 7 days | floodwater recedes over days, not hours |
health | 30 days | outbreaks are long-lived |
severe-weather | 36 hours | official weather warnings are short-lived |
tsunami | 2 days | warnings clear fast once the wave passes |
volcano | 14 days | eruptive episodes persist |
wildfire | 14 days | large fires burn for weeks |
A destination page shows an alert banner only while the alert is active and unexpired andits source is enabled and fully promoted — an unknown or still-in-trial source fails closed and surfaces nothing. Region pages roll up their member countries’ alerts.
Readers who follow a destination can get an email when an alert fires. Sends are claim-first: the send is recorded before it happens, so a retry can never double-deliver — one alert, one reader, at most one email, ever. If a source withdraws an alert, followers who were notified get an explicit correction rather than silence.
Each follow carries a sensitivity setting in plain terms: confirmed alerts only; confirmed plus serious unconfirmed reports; or everything including early unconfirmed reports. Today only the first tier can fire — see the trust stage below.
{
"severity": "Severe", // always a word
"confidence": "Confirmed",
"source": "GDACS", // named, every time
"asOf": "Jun 11, 2026, 9:40 AM UTC",
"disclaimer": "Informational only — …"
}Every new source onboards with a shadow flag: it is ingested, normalized, gated, and scored exactly like a live source — and surfaces nothing. Its would-have-fired record accumulates on an operator dashboard where false positives are visible before any reader could have seen them. Promotion is a deliberate, audited flip, made only after the trial earns it; sources that fail the trial are retired without a reader ever knowing they were tested.
Every published alert today is Confirmed— relayed from an official source. The vocabulary reserves a second tier for corroborated-but-unofficial reports (“Developing — unconfirmed, verify with officials”), and the outbound gate enforces that nothing below Confirmed can fire. The second tier publishes nothing today.
We are also evaluating an experimental corroboration signal drawn from official government accounts on social media — it runs in shadow, is judged server-side against the same rules as everything else, and cannot create or surface an alert on its own. Its trial works like every other shadow trial, with a stricter evidence rule: the pipeline issues precisely-scoped queries, accepts only citation links back — never narrative text — and judges every response server-side against an allowlist before a single row is recorded. By construction there is no code path from that signal to a published alert.
{
"source": "(in trial)",
"ingested": 412,
"wouldSurface": 9,
"falsePositives": 2, // the number that decides
"surfacedToReaders": 0 // always, in shadow
}An alert is always a relay of a named official source. There is no editorial path, no AI path, and no social-media path to a published alert.
One real-world event is one alert with one history. Terminal states are absorbing — what resolved stays resolved.
Unknown source? Still in trial? Pipeline dark? The answer is always the same: surface nothing. Every gate defaults to silence, and the published numbers above are rendered from the enforcing configuration itself.
Coverage is the destination catalog and the hazard types in the table above, on a polling cadence. The absence of an alert is never an all-clear — and the page you’re reading says so in plain language too.