This repo powers RETINA, a passive-radar system: receiver nodes detect aircraft from reflections of broadcast transmitters, and the backend turns those detections into live tracks shown on a web map. Its surfaces are the live map and the admin dashboard.
New here? Start with
ONBOARDING.mdfor the full picture and local setup, anddocs/architecture.mdfor how the pieces fit together.
Finding a suitable broadcast illuminator near a receiver is tower-finder-service,
a separate repo and container. It owns both halves: the API (/api/towers,
/api/elevation, /api/config, /api/geocode) and the UI, which it serves itself
on towers.retina.fm through its own edge.
What remains here is the proxy seam. api.retina.fm/towers forwards to the
service for callers that want a clean public API name, and the other vhosts
still forward /api/towers, /api/elevation, /api/config and /api/geocode
so that a request arriving at one of them reaches the single implementation
rather than a 404 from this backend.
backend/ Python API (FastAPI)
dashboard/ The console, live map included (React/Vite)
packages/shared/ Code the console shares, imported as @retina/shared
e2e/ Playwright suite run after each deploy
docs/ Architecture, pipeline, runbook, simulation, arc-display
libs/ Git submodules
retina-geolocator/ Bistatic passive radar geolocation solver
retina-tracker/ Multi-target Kalman tracker with anomaly detection
retina-custody/ Chain-of-custody signing for node detections
retina-analytics/ Coverage and detection analytics
retina-simulation/ Synthetic node fleet and world model
git clone --recursive https://github.com/offworldlabs/retina-server.git
cd retina-server
# If already cloned without --recursive:
git submodule update --init --recursivejust setupThat is the supported path: it initialises the submodules, builds the backend venv
with uv, installs all five libs/ packages editable, seeds backend/.env from
the example, applies the database migrations (backend/data/users.db does not
exist yet on a fresh clone, and create_all no longer builds it outside the test
suite), and installs the web dependencies, one npm ci at the root for the
console, the shared package and the browser suite together. Install all five even if you only
care about tower search: retina-simulation imports the other four, so a partial
install fails at import time rather than at use.
Then either run the whole local stack:
just up # uvicorn + synthetic fleet + Vite, with hot reload
just status # what is alive
just down # stop itor just the API on its own:
cd backend && .venv/bin/uvicorn main:app --reloadThe API runs at http://localhost:8000, with its reference at / (published at
https://api.retina.fm) and the whole schema at /api/admin/openapi.json. The tower
search is not part of this process: run tower-finder-service (its own repo and
container) if you need /api/towers, /api/elevation, /api/config or
/api/geocode locally.
The console, live map included, does not need it.
The schema is owned by Alembic (backend/migrations/). create_all runs only
in the test suite, which sets RETINA_SCHEMA_SOURCE=create_all; everywhere else
migrations are applied on every start, by just up locally (just migrate runs
it on its own) and by deploy/start.sh on every container boot. Pulling a branch
that adds a revision therefore needs nothing extra.
To change the schema, edit the models, then from backend/:
uv run alembic revision --autogenerate -m "what changed"
uv run alembic upgrade headReview the generated file before committing. Anything other than create_table
must go through op.batch_alter_table, because SQLite cannot ALTER.
RETINA_DB_PATH points Alembic at a scratch file if you want to try a
migration without touching backend/data/users.db.
just setup already installed the dependencies, and just up runs this alongside
the backend. To run it on its own:
npm run dev -w dashboardOpens at http://localhost:5174 on the live map; the admin console is at
http://admin.localhost:5174. API calls are proxied to the backend during development.
The five endpoints under /v1/nodes that receiver nodes talk to are a versioned
wire contract, published at contracts/nodes-v1.openapi.yaml.
The node client and the conformance harness are independent implementations of
it, so it is the one part of this API with consumers holding a pinned version.
That file is generated, not written. It comes from the routes and the Pydantic models, and CI regenerates it and fails when it differs from what is committed, which is what stops the file being edited to match a change instead of the change being noticed. Change a node route, then:
cd backend && RETINA_ENV=dev .venv/bin/python -m scripts.generate_openapiand commit the result alongside. Behaviour a schema cannot carry (whether a
refusal may be retried, and whether retrying can ever help) is annotated onto
the routes as x-retry and x-terminal, and the vocabulary is defined in the
contract's own description. A breaking change raises NODE_API_VERSION in
backend/routes/nodes.py.
Answered by tower-finder-service, not by this backend. nginx proxies all
four to that service on every vhost that answers /api/ (see
deploy/nginx/snippets/towers-proxy.conf and the TOWER_FINDER conditional in
deploy/nginx/nginx.conf.template); this repo keeps the SPA that calls them and
the routing, and no longer keeps a second implementation of the search, the
ranking engine or the Maprad/FCC clients. Parameters, response shape and the
ranking rules are documented in the tower-finder-service repo, which owns them.
The contract those routes must honour before a vhost is pointed at the service
is asserted by deploy/tower-contract.sh, run from CI and from the smoke tests.
- Backend: Python 3.11+, FastAPI, httpx
- Frontend: React 18, Vite, Leaflet
- Tower search: tower-finder-service (Maprad.io GraphQL, ACMA RRL, FCC ULS, ISED SMS)