Real-time 3D visualization of ADS-B aircraft, with historical playback, 3D airway-density heatmaps, and optional ACARS message decoding. One Docker image: it serves the viewer and reverse-proxies your existing ADS-B feeder. Runs on your desktop, your phone, and, with a headset, your coffee table.
Works with anything that publishes readsb's aircraft.json — tar1090,
ultrafeeder, dump1090-fa, readsb-protobuf, and so on.
Live demo → — explore a running instance in your browser.
Warning
Ran the older monolithic version? This is a ground-up rewrite and carries breaking changes — read Upgrading before you pull.
This project is better because people showed up:
- @tyzbit — requested VR support, then became the entire hardware QA department for it: two rounds of Quest 3 testing with annotated videos, the bug isolation that cracked the AR rendering freeze, the control-scheme design that became free-fly mode, the altitude-scale idea (#8) that became the vertical scale slider, and the VR/AR demo video below.
- @ValkyrieUK — built the full-stack Docker integration test suite and CI workflow (#9), and caught a bug that silently broke retention on every fresh install.
- @unLieb — scoped the localization architecture before a line was written (#10) and is the native-speaker reviewer for the German translation.
- @rknobbe — asked the "can it render the mountains?" question (#7) that became 3D terrain.
Want your name here? Issues with reproduction steps, hardware testing, and translations count just as much as code.
- AR diorama mode, matured. The scope clips to an open-top box you place on real furniture and resize from the wrist menu; panning slides the map beneath the fixed frame (yaw-only, so a tilted head never lifts the floor). New auto-orbit slowly circles the scope center or a followed aircraft for a proper desk-ornament view, with an optional follow-random-aircraft mode that re-targets the chase when the followed aircraft drops off the feed. A high-res basemap option fetches tiles one zoom level sharper — low-res tiles are the first thing a headset makes obvious.
- v0.9.0: the hardening release. A full-project audit in one drop: security headers with a configurable CORS lockdown, per-client rate limiting behind reverse proxies, backend loops that survive bad upstream data and report honest healthchecks, a broad frontend performance pass, keyboard and screen-reader access for the aircraft list, and test coverage from one placeholder test to 750+ real ones with eslint, ruff, and pytest wired into CI.
- v0.7.0: aircraft that look like aircraft. Every common silhouette now has a real 3D body built from its own tar1090 drawing: a lofted fuselage that follows the drawn width, engines with intake lips on the drawn pods, tail fins, a proper T-tail on the C-17, and rotor blades on helicopters at the angles the artist drew.
Full history in CHANGELOG.md and on the releases page.
Live mode — per-second updates with a tar1090 altitude color palette
across cones, trails, ground icons, and labels. Click an aircraft for a
detail card: photo, filed route, airframe, and autopilot data when
broadcast. Filter pills (All / Air / Ground / Mil / Emerg) drive both
the list and the 3D scene; emergency squawks get a pulsing red ring.
Mobile-friendly — the sidebar collapses and settings open as a sheet.
Historical mode (needs track-service + TimescaleDB) — scrub a timestamp cursor across the last 1h / 24h / 7d at 1×–60× speed. The 3D airway-density overlay renders every flight path at its real altitude, so busy airways and approach corridors light up as bright bundles in the sky.
ACARS (needs acars-service) — per-aircraft datalink messages in the detail card with an OOOI flight-phase chip (taxi-out / airborne / taxi-in / at gate), a searchable full-page browser, and a 3D ping ring when a message lands for an aircraft on scope.
Multi-feed — point at several receivers and the status bar grows a feed picker. Switching is in-place — no page reload.
Themes — five built-in palettes (Midnight Glass, Daylight, Sectional
Chart, Phosphor CRT, High Contrast) pickable from the settings panel.
Auto follows your system light/dark preference. Switching is live; the
3D scene re-tints in place. Plays especially well with the FAA chart
basemaps below.
Languages — English, German, and Spanish (translations are
machine-drafted pending native review — corrections welcome). Auto
follows your browser locale.
3D terrain (opt-in) — the basemap rises to real ground elevation, with range rings, markers, and aircraft ground icons draped over the hills and an above-ground-level readout in the detail card. Free elevation data, no API key. Pairs beautifully with the OpenTopoMap basemap. With terrain off, the flat map stands at the home field's elevation, so landing traffic meets the map instead of floating at field-elevation height above it. A companion altitude scale slider warps the vertical axis toward low-altitude detail (pattern traffic spreads out) or high-altitude detail (flight levels spread out) — terrain and aircraft stay consistent at any position.
VR / AR — immersive WebXR sessions with laser-pointer controllers, a wrist menu with full settings parity, comfort locomotion (scope or free-fly, snap or smooth turning), and AR placement of the scope onto real furniture. The diorama clip box turns AR into a desk ornament: airspace confined to a placeable, resizable open-top box, with follow mode, slow auto-orbit, and an optional random-aircraft chase. Hardware-tested on a Quest 3; see VR & AR below for the demo video. Side-by-side stereo (Cardboard) is still there for anything without WebXR.
High-res basemaps — an optional sharper-tile mode fetches the basemap one zoom level deeper (4× the tiles for the same coverage). Worth it on a headset or a 4K display; costs bandwidth accordingly.
Keyboard & screen readers — the aircraft list is a real listbox: arrow keys move, Enter/Space selects, and screen readers get proper roles, a focus-trapped ACARS dialog, and a throttled live aircraft count instead of per-second chatter.
FAA aeronautical charts (US only) — Sectional, Helicopter, IFR Low, IFR High, and a sectional + roads hybrid, served through the same tile proxy as the regular basemaps. The container auto-discovers the current FAA 56-day chart cycle at start, so the upstream URL stays valid through each rotation as long as you restart the container occasionally.
Voice scanner (needs a separate voice stack) — an optional VHF airband call feed, shown only on the local feed. See docs/VOICE.md.
- Docker Engine 20+ and Docker Compose v2
- An ADS-B feeder already publishing
aircraft.json(tar1090, ultrafeeder, dump1090-fa, readsb-protobuf, …)
services:
adsb-3d:
image: ghcr.io/hook-365/adsb-3d:latest
ports: ["8086:80"]
environment:
- LATITUDE=45.0000
- LONGITUDE=-90.0000
- ALTITUDE=1000
- LOCATION_NAME=My Station
- FEEDER_URL=http://ultrafeederdocker compose up -d, then open http://localhost:8086/ — your
aircraft should appear within a few seconds.
FEEDER_URL must be reachable from inside the container. A bare
service name like http://ultrafeeder only resolves if adsb-3d shares a
Docker network with your feeder. If it doesn't, use the feeder's host IP
and port — e.g. http://192.168.1.50:8080. If the page loads but stays
empty, this is almost always why: check docker logs adsb-3d (the
container reports unhealthy until it can reach the feeder).
For historical playback, ACARS, or the voice scanner, copy
.env.example to .env and start from docker-compose.example.yml
in the repo root. Both have track-service, ACARS, and TimescaleDB
ready to uncomment; the ENABLE_* flags below switch each on.
Put the airspace on your desk. With a WebXR headset the scope becomes a walkable diorama: orbit a selected aircraft, free-fly through the traffic, change settings from a wrist menu, or enter AR and drop the map onto real furniture with a glance and a trigger pull. Diorama clipping keeps it desk-sized — the map pans beneath a fixed, resizable open-top box — and auto-orbit slowly circles the scope (or your followed aircraft) like a live model.
adsb3d-demo-clip.mp4
Demo recorded on a Quest 3 by @tyzbit, who hardware-tested every iteration of these features. The full-length demo is attached to the v0.6.0 release.
adsb-3d runs on a subdomain or a subpath (example.com/3d). The
entrypoint auto-detects the subpaths /3d, /adsb, and /adsb-3d when
your proxy passes the prefix through; set BASE_PATH if the proxy strips
the prefix or uses a different path. Worked configs for nginx, Traefik,
Caddy, Apache, and Nginx Proxy Manager are in
docs/REVERSE-PROXY.md.
Define each feed with flat FEEDN_* env vars — the entrypoint
synthesises the rest:
FEED1_NAME=Home Station
FEED1_LAT=45.0000
FEED1_LON=-90.0000
FEED1_ALT=1000 # feet MSL, like all altitudes here
FEED1_ACARS=true # optional
FEED2_NAME=Remote Site
FEED2_URL=192.0.2.10:8086 # host:port of another adsb-3d instance
FEED2_LAT=43.0000
FEED2_LON=-89.0000
FEED2_COLOR=#ff8c4c # optionalSlot 1 is always local — FEED1_URL is ignored. Slots 2+ point at other
adsb-3d instances and the entrypoint wires up the nginx proxy blocks.
Parsing stops at the first missing FEEDN_NAME.
Core:
| Variable | Default | Purpose |
|---|---|---|
LATITUDE / LONGITUDE / ALTITUDE |
— | Receiver location; ALTITUDE in feet MSL (as are all FEEDN_ALT) |
LOCATION_NAME |
Home |
Display name |
FEEDER_URL |
http://ultrafeeder |
Anything publishing /data/aircraft.json — must be reachable from the container |
ENABLE_HISTORICAL |
false |
Historical playback UI (needs track-service) |
ENABLE_ACARS |
false |
ACARS panel (needs acars-service) |
ENABLE_VOICE |
false |
VHF voice scanner panel (see docs/VOICE.md) |
ENABLE_TERRAIN |
true |
Deploy-level 3D-terrain kill switch (users still opt in per-browser) |
HIDE_TOWER |
false |
Hide the home tower marker |
TRACK_API_HOST |
track-service:8000 |
nginx upstream |
ACARS_API_HOST |
acars-service:8000 |
nginx upstream |
VOICE_EVENTS_HOST |
— | nginx upstream for /voice/calls + /voice/ws — what the frontend uses |
VOICE_STREAM_HOST |
— | required when voice is on; point at any reachable host:port (legacy Icecast block, not played by the frontend) |
DNS_RESOLVER |
127.0.0.11 |
nginx resolver address(es) for upstream lookups; override on runtimes without Docker's embedded DNS (e.g. Kubernetes CoreDNS) |
MAP_ZOOM / MAP_GRID_SIZE |
8 / 21 |
boot-time tile pre-cache around the station (zoom level / grid width); only active with the /tiles volume mounted |
Multi-feed: FEEDN_NAME, FEEDN_LAT, FEEDN_LON, FEEDN_ALT,
FEEDN_URL, FEEDN_COLOR, FEEDN_ACARS — see Multi-feed.
(FEEDS_CONFIG accepts the raw JSON feed array directly and skips the
FEEDN_* synthesis — see .env.example for the shape.)
Backend services (track-service / acars-service containers, not
the main image — see the compose example):
| Variable | Default | Purpose |
|---|---|---|
FEEDER_URL |
http://ultrafeeder |
same feeder the frontend uses; both services poll it |
FEEDER_POLL_SECONDS |
heuristic | live-stream poll cadence override (defaults per feeder type) |
COLLECTION_INTERVAL |
5 |
seconds between DB track snapshots (track-service) |
RETENTION_DAYS |
90 |
TimescaleDB retention for tracks and ACARS messages |
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD |
timescaledb-adsb / 5432 / adsb_tracks / adsb / — |
TimescaleDB connection; password has no default on purpose |
ACARS_HOST / ACARS_PORT |
acarshub / 15550 |
acarshub JSON-over-TCP feed (acars-service) |
STATION_ID |
adsb-3d |
station identifier stamped on stored ACARS messages |
Reverse proxy: BASE_PATH overrides the auto-detected subpath
(see Reverse proxy). TRUSTED_PROXY_CIDR (comma-
separated CIDRs) makes rate limiting key on the real client IP behind
a fronting proxy instead of one shared bucket. CORS_ALLOW_ORIGIN
(default empty = no CORS headers) opts a specific origin — or * —
back in if another site consumes the API directly.
Mouse: left-drag orbits the camera, scroll zooms, right-drag pans the view (moves the center point).
| Key | Action |
|---|---|
| Arrow keys | Pan the view across the map |
R |
Recenter camera + clear selection |
/ |
Focus the list search box |
Esc |
Close settings panel or ACARS browser |
The older version was a single ~14k-line vanilla-JS app; this is a TypeScript / Three.js rewrite. Most deployments keep working after a pull, but review these first:
ENABLE_HISTORICALdefaults tofalse(wastrue) — set it explicitly totrueif you runtrack-service.ENABLE_VOICE=truenow requiresVOICE_STREAM_HOST+VOICE_EVENTS_HOST— the container fails fast without them.track-service/acars-servicerun as non-root (uid10001) — host paths bind-mounted into them must be writable by that uid.
Full detail in CHANGELOG.md.
┌──────── browser ────────┐
│ Vite/TS Three.js app │
└───────────┬─────────────┘
│
nginx (port 80)
│
┌──────────┬──────────┼──────────┬──────────────┐
│ │ │ │ │
/data/... /api/... /ws/... /api/feeds/N/... /acars-api/
│ │ │ │ │
ultrafeeder track-service │ remote adsb-3d acars-service
│ instance (slot N)
▼
TimescaleDB
(aircraft_positions
+ aircraft_metadata
+ acars_messages)
frontend/— Vite + TypeScript + Three.js viewer, no framework.track-service/— FastAPI + asyncpg; a live WebSocket diff stream plus a TimescaleDB history collector.acars-service/— FastAPI bridge to an acarshub TCP feed.nginx/— reverse proxy + static host;entrypoint.shrenders the config (including per-feed proxy blocks) from env vars at start.
See CLAUDE.md for a deeper architecture orientation.
cd frontend
npm install
npm run dev # Vite dev server
npm run typecheck # tsc, strict
npm run test # Vitest
npm run build # → dist/npm run dev proxies backend routes to http://localhost:8080; override
with DEV_BACKEND. To run the full container against an existing backend:
docker compose -f docker-compose.dev.yml --project-directory . up --build -d
# → http://localhost:8186/- tar1090 (wiedehopf/tar1090, GPL v2+) — SVG aircraft shape catalog and the altitude → color palette.
- readsb / dump1090-fa — upstream Mode S/ADS-B decoder.
- planespotters.net — aircraft photographs in the detail panel.
- adsb.im — callsign → route resolution.
- OpenStreetMap, Carto, ESRI, OpenTopoMap — basemap tile providers.
- VFRMap — hosting for FAA Sectional / Helicopter / IFR Low / IFR High chart tiles, kept in sync with the FAA 56-day cycle. Free non-commercial service; please don't abuse it.
Source code is MIT (see LICENSE). Vendored tar1090 data
(frontend/src/aircraft/shapes-data.json) is GPL v2+ per upstream — if
you redistribute the built app, GPL governs that component.



