The aircraft page renders live traffic over my house: positions, headings, altitudes, distance from the receiver, with a Flightradar24 link as an external cross-check. The page is the easy part. The design work is in the pipeline behind it, and the guiding principle throughout was strict separation of concerns — each stage does one job and stays replaceable.
Start at the edge. An SDR feeds dump1090-fa on a Raspberry Pi, and that process owns RF ingest and ADS-B decoding entirely. It writes current state to /run/dump1090-fa/aircraft.json. Nothing downstream re-implements any of that. This is a deliberate boundary: the decoder is a well-understood, single-purpose component, and I treat its JSON output as a stable contract. If I swap decoders later, everything above this line is unaffected.
A separate Ruby service, adsb_push, owns the transport concern. On a one-second interval it reads the file, normalizes rows to a minimal payload, ages out stale aircraft, dedupes by hex, and publishes one snapshot over an Action Cable websocket. Two design choices matter here. First, each frame is self-describing — it carries its own timestamp and the receiver coordinates — so consumers never depend on out-of-band state. Second, the service is built as infrastructure, not a script: it reconnects on failure, backs off when the cable is down, and runs under systemd. That gives me independent failure domains. The decoder and the publisher can each die and recover without taking the other with them.
Rails owns everything from access control inward. The channel authenticates two distinct roles — a publisher presenting a private ingest token, viewers presenting a public viewer token — and never conflates them. Inbound snapshots get normalized once more and rebroadcast to subscribers. Centralizing that normalization is intentional: there's exactly one place that defines the payload shape the page sees, which keeps the contract enforceable. Consolidating the frontend back into Rails removed a whole class of problems — no separate SPA build, no duplicated client config, no second deploy target. Page configuration that used to be fetched and rehydrated client-side is now just rendered server-side at request time.
Presentation lives in the Rails views, with Stimulus driving the parts that have to be dynamic. A controller opens the Action Cable subscription, computes distance from the receiver, and maintains capped per-aircraft trails so memory stays bounded under long sessions. It degrades predictably — a snapshot timeout drops it out of live mode, a demo mode covers the no-feed case, and a debug panel exposes connection state and frame timing for when I need to diagnose rather than admire. Mapbox still does the actual drawing: custom markers per aircraft, a "Receiver" pin for home, smoothed trail lines.
The payoff of the layering isn't elegance for its own sake; it's operability. Because the boundaries are clean, the page doubles as an end-to-end health check. Fresh traffic on the map means the SDR, the Pi service, the cable, and the view are all healthy. When something breaks, the same separation tells me which layer to look at. That's the property I was optimizing for.