κεραυνός: the thunderbolt.
Every lightning strike on earth, as it is detected. Open the instrument →
Live strikes from Blitzortung, plotted on a world map as they arrive. Strikes flash and fade, busy cells burn into the map, and clusters are tracked as storm cells with heading, speed, and a jump ring when flash rate spikes.
No account, no key. A relay holds one upstream connection to Blitzortung and passes frames through unchanged. Nothing is stored server-side; everything the page derives is lost when the tab closes.
| Point | Reads out place, coordinates and strike count for the 1° cell under the pointer |
| Pick | Click the map, a feed row, or a place to narrow to it; click a cell to narrow to the cell. esc clears |
| Move | Drag to pan, wheel/pinch to zoom to ~200 km across. Region names along the top jump there |
| Rewind | An hour of history on the bottom track, handed over full at session start. Drag to scrub; ×8/×30 catch up to live, life size keeps pace |
| Here | Browser geolocation on request, frames the map on it, shows distance and thunder countdown to the nearest strike. Session only |
| Link | Zoomed in, the URL carries #lon/lat/zoom |
| Save | Configuration exports retained strikes as CSV, or the current frame as PNG |
| Hold | Feed stops advancing while the pointer rests on it, queues arrivals behind |
| Guide | Seven-step walkthrough, opens itself on first visit; g afterwards, esc to skip |
| Data | d: every session figure, grouped by sky / network / instrument |
Keys. k key panel · c configuration · d data · g guide · t dark/light ·
esc close panel or clear selection. +/- zoom, 0 resets view. Rewind
track takes arrow keys, home, end.
| Dial | Stops | |
|---|---|---|
| view | flat · globe | Flat map, or the same data on a draggable sphere |
| field | off · cloud · rain | Background layer; see below |
| cells | off · ring · track · full | Ring: cell + count. Track: + history. Full: + heading and speed |
| burn | 4m · 20m · 1h | How far back the burn-in reaches |
| Reading | |
|---|---|
| Rate | Strikes/min over the last 60s, with trace, and cell count. ↑ marks cells in a lightning jump |
| Session | Arrivals by the minute for the tab's lifetime, midnight UTC marked, peak minute called out |
| Link | Median delay strike→browser, median detector count per strike |
| Reach | How far strikes were heard, day vs night. Night runs longer: ionospheric reflection height rises from ~60-70 km (day) to ~85-90 km (night) |
| Here | Distance to nearest strike and thunder countdown, once here is pressed |
| Most active | Places with cells currently burning, click to narrow |
| Strike feed | Each arrival as it lands; click to narrow |
c opens it. Stored in-browser only.
| Group | What it holds |
|---|---|
| Tube | Phosphor colour (white/green/amber/ice/oil/crimson/demon, dark only), contrast, bloom |
| Layout | Side panel, header, footer visibility |
| Screen | Scanlines, sweep, strike shake, detector clicks, thunder |
| Map | Cell bounds, graticule, frontiers, daylight, capitals, detector threads, strike persistence |
| Panel | Which readings are shown |
| Session | CSV / PNG exports |
Thunder needs a position set first: it's the delay between a strike and its sound reaching there.
Blitzortung is volunteer hardware; each strike is a time-of-arrival fix from stations someone hosts. Detector threads draw a line from a strike to every station that helped place it. A full sheaf means the strike was pinned from all sides; a fan means it was heard from one direction. Typical strikes have a gap past 200° in their ring of stations. Detector positions aren't published; the map infers them from strikes over ~30s of listening.
Infrared (cloud) reads storm tops from orbit; radar (rain) reads what's falling, from ground stations. Alternatives, not layers — they draw the same storm where they overlap. Cloud covers the whole planet; rain only where ground radar exists, so empty means "clear" on one and "unwatched" on the other.
Up to an hour, capped at 120,000 retained strikes (shorter at high global rate). A session opens on whatever window the relay is holding. Scrubbing resumes forward playback rather than freezing a frame; ×8/×30 speed catches up to live. Storm rings and tracks are replayed too, walked to the scrub position at their own 20s sampling cadence. Bolts and the chassis knock aren't replayed.
Capitals light up only while a burning cell is within 400 km, fading with it. Pointing at the map names whatever's under the cursor on demand.
The stream isn't archived; closing the tab loses the retained hour.
CSV: flash_utc, received_utc, lon, lat, one row per retained strike.
PNG: the frame as drawn, at whatever scrub position — a link can't carry
strikes, only coordinates, so a shared URL opens on live weather instead.
Blitzortung asks that consumers relay from their own server rather than
connecting per-client. relay/ is that server: a single Cloudflare Durable
Object with one upstream connection, fanning frames out to every visitor.
It also retains the last hour (a few hundred KB) and hands it to a new tab as one binary frame before the live feed starts, so a fresh session doesn't open on an empty map. Persisted every 10s in 5-minute buckets so a deploy or eviction costs nothing; no reader state is stored anywhere.
npm install
npm run dev # dev server
npm run build # production build
npm run lint
npm run check # checks the palette, the geometry and the fetched fields against live dataNeeds a relay for strikes. Deploy to your own Cloudflare account, or run locally:
cd relay && npm install
npm run dev # serves ws://localhost:8787/feed
npm run deploy # to your account, once wrangler is logged inThen point the site at it: copy .env.example to .env.local, set
VITE_FEED_URL. Set ALLOWED_ORIGINS in relay/wrangler.jsonc before exposing
the relay publicly, or anyone who finds the hostname can spend its single
socket.
No API keys anywhere.
node scripts/KeraunosSeeker.cjs: standalone Node client, connects to
Blitzortung directly and prints the stream to the terminal.
Screenshots in this file are captured from the running instrument:
npm run shots # all of them, into docs/shots and public/og.png
npm run shots -- hero # one
VIEW=-99.4/41.2/6 npm run shots -- hero # framed somewhere else
URL=http://localhost:5173 npm run shots # against the dev server rather than productionEach shot soaks briefly before firing (a fresh load is a black rectangle).
Pick a VIEW that's active at run time, or the shot shows nothing happening.
For watching weather, not deciding what to do about it. Blitzortung's data is for private/entertainment use only, explicitly not for storm warning, overvoltage claims, or risk analysis, and not for protection of life or property — neither is this. The distance-to-nearest-strike and thunder countdown readings are arithmetic on a feed that misses most cloud-to-cloud discharge and anything outside detector range. A quiet map is not a safe sky.
For decisions, use your national meteorological service.
The MIT licence covers the code here only.
Strikes: Blitzortung and its volunteers, under Blitzortung's own terms (private/non-commercial, no storm warning). Not this project's to relicense; a self-hosted instance is bound directly by those terms.
Weather fields: read live, not redistributed. Cloud from RealEarth (SSEC, UW-Madison) and EUMETSAT; rain from RainViewer. Both restrict redistribution and commercial use.
Fonts: IBM Plex Mono, SIL OFL 1.1, licence in public/fonts/OFL.txt.
Geometry: src/lib/world.json — Natural Earth 110m Admin 0, public domain.
src/lib/us.json — Leaflet's US states example, derived from US Census
boundaries, public domain. src/lib/water.geo.json and unused predecessor
src/lib/water.json — named water bodies, provenance untraced.
Palettes: borrowed phosphor hues credited in src/lib/palette.js.



