Skip to main content
Glama
README.md
# Velocity

A self-hosted live map of aircraft, ships and satellites that records what it
sees, so you can rewind to any past moment. No API keys, no account.

[![License](https://img.shields.io/badge/license-AGPL--3.0-orange.svg)](./LICENSE)
[![Version](https://img.shields.io/github/v/release/AndrewCTF/velocity?label=release&color=blue)](https://github.com/AndrewCTF/velocity/releases/latest)
[![Tests](https://img.shields.io/badge/tests-3276%20passing-brightgreen.svg)](#tests)
[![No keys required](https://img.shields.io/badge/API%20keys-optional-success.svg)](#what-it-pulls-in)

<p align="center">
  <img src="docs/media/hero-replay.gif" alt="Live world view, fly-in to Europe, an aircraft selected with its owned track, then the last hour rewound on the replay scrubber" width="900">
</p>
<p align="center"><i>One take, real data: the live world, zoom to Europe, click an aircraft, see its track, rewind the last hour.</i></p>

```bash
git clone https://github.com/AndrewCTF/velocity.git && cd velocity
docker compose up          # then open http://localhost:8080
```

## Why it exists

Free trackers only let you look back a little: Flightradar24 gives 7 days of
replay on the free tier, MarineTraffic 24 hours, and ADS-B Exchange dropped its
free API. Velocity pulls the same public feeds and writes every position to
your own disk, so the history is yours.

|                | History you can replay            | Self-hosted | Account / API key | Who owns the archive |
| -------------- | --------------------------------- | ----------- | ----------------- | -------------------- |
| **Velocity**   | As much as your disk holds        | Yes         | None              | You                  |
| Flightradar24  | 7 days on the free tier           | No          | Account           | Them                 |
| MarineTraffic  | 24 hours free (down from 72)      | No          | Account           | Them                 |
| ADS-B Exchange | Free API tier discontinued        | No          | Paid API          | Them                 |

Free tiers as of July 2026; each cell is checkable on the vendor's pricing page.

## What it does

- **Records history.** Every position goes into SQLite by default, or
  Postgres/TimescaleDB if you set `HISTORY_PG_DSN`. The default keeps a rolling
  window; set `ARCHIVE_MODE=1` to keep everything. A scrubber at the bottom
  rewinds the map to any moment you recorded.
- **Says where each contact came from.** Every aircraft and ship shows which
  feeds reported it, how many agreed, and how old the position is, so a stale
  fix doesn't look live. `/api/status/provenance` gives the same per feed.
- **Puts more on the same map.** GPS jamming, ships with AIS switched off,
  flight restrictions, military bases, naval warnings, earthquakes, internet
  outages and conflict events.
- **Keeps evidence.** Captures get a SHA-256 hash and an append-only custody
  log, and a case exports to an HTML or PPTX report that cites its sources.
- **Talks to AI agents.** It runs as an MCP server, so an agent can ask about
  live traffic instead of guessing. Anything automated is labelled as such.

[Quick start](#quick-start) · [The apps](#the-apps) · [Take the tour](#take-the-tour) · [Query it from an AI agent](#mcp-server-query-the-live-console-from-an-ai-agent)

## Limits

- It's built for one person, not a team.
- Coverage is whatever the public feeds see. It's good over Europe and North
  America and thin elsewhere; ship data is densest around Northern Europe. The
  map tells you where coverage is thin.
- Some live state (current incidents, area selections, briefs) clears on
  restart. History, saved objects, cases and the evidence log stay on disk. See
  [Scope and limits](#scope-and-limits).
- The 3D satellite view needs a lot of GPU memory.

**Use at your own risk.** No warranty; the authors take no responsibility for
how you use this tool or the data it pulls. Velocity pulls from third-party
sources that have not authorised it: their terms apply to you, the requests
come from your IP, and rate limits, bans and legal exposure are yours. Feeds can
be incomplete, delayed or spoofed, and AI summaries can be wrong. Don't use it
for navigation, safety-of-life, or anything where being wrong hurts someone.
Read [`DISCLAIMER.md`](./DISCLAIMER.md) before you run it. Security
self-assessments: [ISO/IEC 27001:2022 controls](docs/security/isms/statement-of-applicability.md),
[OWASP ASVS 5.0 L2](docs/security/asvs-l2-assessment.md).

## Prerequisites

Honestly, not much. There are two ways to run it, pick whichever you're set up for:

- **The easy way, with Docker.** If you have Docker (24 or newer, with the
  `compose` plugin that ships with Docker Desktop), that's the whole list. One
  command and you're up.
- **Running the pieces yourself.** If you'd rather not use Docker, you'll need
  Node 20+, pnpm 9 (`corepack enable` picks the right version for you), Python
  3.12 for the backend, and `uv` to pull its dependencies. A plain venv works too
  if you don't have `uv`.

Either way, you need a browser that can do WebGL2, so any recent Chrome, Edge or
Firefox. The globe leans on your GPU, so on a laptop with switchable graphics do
yourself a favour and push it onto the discrete card before you judge the frame
rate.

And no keys. The core feeds (planes, ships, quakes, satellites, the basemap) all
run without a single API key. Keys only ever add reach; see
[What it pulls in](#what-it-pulls-in) for what each one buys you.

## Quick start

Docker is the short road. It brings up the API, the web app and nginx together:

```bash
git clone https://github.com/AndrewCTF/velocity.git
cd velocity
cp .env.example .env       # optional, leave it empty and it still works
docker compose up          # api + web + nginx on :8080
```

Now open <http://localhost:8080>. It comes up live, planes moving, ships,
quakes, the lot, with nothing to configure in between — and position history
is already recording to a Docker volume, so it's still there the next time you
run `docker compose up` (set `ARCHIVE_MODE=1` in `.env` for an open-ended
archive instead of the default rolling window; see
[Scope and limits](#scope-and-limits)). The first time in, a short tour points
out where things live; you can pull it back up whenever from **⚙ Settings**.

![The time dock in replay: the window set to one hour, the incident and signals density strips populated across it, and the header reading "Replay · 03:00 to 04:00 Z" with a return-to-live button](docs/media/replay-scrubber.jpeg)

That dock along the bottom is the archive. Pick a window (1h, 6h, 24h, 3d, 7d),
click anywhere on the density strip, and the globe rewinds to that moment; the
header swaps from _Live_ to _Replay_, and one button puts you back. Playback
runs at 1x through 3600x. The dated day-picker and the _recording since … · GB
· fixes_ ownership chip live in the 2D console at `/2d`, which reads the same
`/api/history/coverage` archive.

The Play button drives the clock through half-hour tar1090 replay chunks with a
24-entry cache, decoding readsb's 16-byte heatmap entries in a Web Worker and
appending each contact's samples to its `SampledPositionProperty`. Aircraft
reach back to 2024 through the public readsb heatmap archives the backend
proxies and caches (`GET /api/history/upstream/chunk|tracks|coverage`); one
decoded adsb.lol chunk (2024-06-01 12:00 UTC) held 180 slices of 10 s and 3,953
aircraft tracks over Europe. Vessels have no public equivalent, so they replay
only as far back as your own archive goes.

> **Local (open) mode.** The compute-heavy endpoints — the LLM-backed Reports
> tabs, recon, and OSINT enrichment — _fail closed_ on a keyless box so a
> public deployment can't be made to spend GPU/credits by an anonymous caller.
> The dev `docker compose up` sets `ALLOW_UNAUTHENTICATED=1` for you (the stack
> is loopback-only), so those features work out of the box and the UI shows an
> open-mode banner. Configure `API_KEY` or Supabase in `.env` and real auth
> takes over (the flag becomes irrelevant). For an internet-facing box use
> `docker-compose.prod.yml`, which leaves the flag unset. The globe, replay,
> evidence capture, and all keyless feeds never needed the flag.

### Open it on your phone

The stack listens on `127.0.0.1` only, on purpose: the dev stack runs in open
mode, so anything that can reach it can use it. To reach your own instance from
your phone, put it on your tailnet instead of your LAN:

```bash
tailscale serve --bg 8080   # https://<this-machine>.<your-tailnet>.ts.net
```

Then add that origin to `.env` and restart, or the API refuses the new `Host`
header (the DNS-rebinding guard in `app/origin_guard.py`):

```bash
CORS_ORIGINS=http://localhost:8080,https://<this-machine>.<your-tailnet>.ts.net
```

Only devices signed in to your tailnet can open it. Don't use `tailscale funnel`,
which publishes it to the whole internet. For anything wider than your own
devices, run `docker-compose.prod.yml` with Supabase sign-in configured
(`docs/security/auth-and-sessions.md`); it fails closed without a credential.

<details>
<summary><b>Local dev without Docker</b></summary>

```bash
make install                                      # pnpm install + api venv
cd apps/api && .venv/bin/uvicorn app.main:app     # backend on :8000
pnpm dev                                          # vite on :5173, proxies /api to localhost:8000
```

Set `API_PROXY_TARGET` if the backend isn't on `http://127.0.0.1:8000`; the dev
server proxies `/api`, `/tiles` and `/ws` there, so the browser stays same-origin.
(`VITE_API_URL` is different: it makes the browser call the API directly,
cross-origin, and is only for builds with no proxy in front.) `API_KEY`
is for servers, the CLI and the MCP server. Never build it into a hosted web
bundle, because anyone who loads the page can read it. Browsers sign in with
Supabase instead (`docs/security/auth-and-sessions.md`).
</details>

## The apps

Velocity isn't one mega-dashboard; it's a workspace of eighteen apps that all
share the same selection and time context, so switching never loses the object
you had selected. The launcher in the top bar groups them:

| Group       | Apps                                                                   | What they're for                                                                                                                                                                                                                              |
| ----------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Live**    | Map · Sim                                                              | The Cesium globe (always mounted behind everything else) and a browser war-game overlay.                                                                                                                                                      |
| **AI**      | AI · Models                                                            | The analyst agent, automated watch and alerts, and a registry of local models (installed, catalog, presets, engine).                                                                                                                                                      |
| **Analyze** | Explorer · Graph · Investigate · Targeting · Video · Country · Markets · Quiver · Vertex | Filter the live object store, grow a link-analysis graph, run digital OSINT (domains/people/wallets), a notional kill-chain board, FMV/ground recon, per-country World-Bank/UN stat dossiers, indices/commodities/FX with a stress index, time-series cards over the recorded archive, and a system graph of sources, feeds and layers. |
| **Data**    | Foundry · Workflows · Workshop                                         | Bring your own data through governed pipelines; automate the live feeds with a node-graph editor; compose modules from live widgets.                                                                                                                                             |
| **Product** | Reports                                                                | Case files, watch-officer briefs, dossiers, and case→report export.                                                                                                                                                                           |
| **3D**      | City 3D                                                                | Gaussian-splat city scenes built from a keyless satellite AOI.                                                                                                                                                                                |

The rest of this README tours the ones you'll live in. Everything below is a
real screenshot of the current build (v1.0.1), taken against live feeds on
2026-08-08, not a mockup. `node scripts/screenshot-readme.mjs` re-shoots the
whole set.

## Take the tour

**1. The whole planet, live.** Thousands of aircraft plus vessels, satellites,
quakes and more on one Cesium globe. Every aircraft and ship renders as its
category icon, coloured and rotated to its heading; dense areas cluster into
count badges you can drill into.

![Global view of the Velocity globe with live air, sea and incident traffic](docs/media/hero-world.jpeg)

**2. Zoom anywhere, toggle everything.** Drag into a region and traffic, labels
and coastlines fill in. The left rail is the whole intel stack in one tree — air,
maritime, space, ground hazards, signals and 18 infrastructure layers — each
with its live count. A right-side map toolbar adds the analyst verbs on top of
the globe: **measure** a running great-circle distance, **area-select** a box
and search every object inside it, **annotate** with dropped markers, or **move**
a marker — plus one-click camera reset and zoom.

![The layer tree over a live European air picture, air/maritime/space/hazards toggles with counts](docs/media/panels/rail-layers.jpeg)

**3. Click anything.** Select an aircraft or vessel and the right panel fills
with its dossier: identity, kinematics, the flight/route, ACARS, a
pattern-of-life baseline, GPS integrity and the raw fields — while a magenta
line traces its recent track on the globe. An **AI assessment** block (labelled,
and grounded in the observed track) summarises what the dossier fields imply.
Click empty space and it all clears.

![A selected aircraft over the North Atlantic with its full dossier panel open — identity, kinematics, integrity, freshness, provenance, sanctions screening — and its magenta track drawn back across the ocean](docs/media/panels/inspector-selection.jpeg)

**4. Or query the whole store at once.** The Explorer app is every live object
in one filterable, sortable table — tens of thousands of tracks across vessels,
aircraft and quakes, each tagged with the feed it came from and how long ago it
was seen. Facet it, save the search, export the result to CSV.

![Explorer: every live object in one filterable table, faceted by kind and tagged by source](docs/media/ui-explorer.png)

**5. Chokepoints and AOIs, watched continuously.** A curated library of the
world's maritime chokepoints — Hormuz, Bab-el-Mandeb, Malacca, the Taiwan
Strait, the English Channel, undersea-cable corridors — each with live transit
counts and the daily oil-flow figures that make it matter. Standing detections
fire against them in the background.

![Global chokepoint watch: Hormuz, Bab-el-Mandeb, Suez, Panama and Malacca with their daily transit and oil-flow figures](docs/media/panels/rail-chokepoints.jpeg)

**6. Airspace, bases, ports and airports — operational context, still keyless.**
Toggle the TFR/Airspace layer and live FAA temporary flight restrictions draw as
real polygons, coloured by reason with their altitude bands. Alongside them:
7,183 military bases (air, naval and army, from Wikidata), NGA NAVAREA broadcast
warnings with mine areas flagged, 3,804 ports from the NGA World Port Index
(harbour size and type, shelter, repairs, dry dock, pier and channel depths),
and airports enriched with runways (length, surface, lighting, per-end ILS
category from FAA NASR), tower/ground/ATIS frequencies, live METAR with flight
category, and a LiveATC linkout. A basemap picker swaps between nine keyless
map styles (Carto dark, 3D satellite + terrain, Apple satellite, Esri
imagery/topo/dark, OpenTopoMap, USGS, Sentinel-2 cloudless), each probed live
before it's offered and each carrying its required attribution.

![Live FAA temporary flight restrictions drawn as polygons over the US, with SIGMET hazards and the airport and port layers on](docs/media/places-airspace.jpeg)

**7. Space, with real physics.** ~16k satellites from CelesTrak, propagated
in-browser with actual SGP4 orbital mechanics and enriched from SATCAT with
owner, launch date, radar cross-section and orbit class.

![The globe wrapped in the live satellite constellation](docs/media/hero-satellites.jpeg)

**8. The watch officer works the inbox.** An autonomous watch officer fuses
jamming, dark vessels, military air activity and conflict events into ranked,
cited alerts — GPS spoofing, jamming cells, dark-vessel candidates — each with an
`[inferred]` reasoning line and a one-click _slew to_. It runs `detect_deception`
and `deep_analyze` against the live picture, so the inbox fills itself on a fresh
keyless boot with no operator setup.

![Watch-officer inbox: queued alerts carrying spoofing, jamming and dark-vessel badges alongside the reasoning behind each one](docs/media/panels/rail-inbox.jpeg)

**9. …and writes the brief.** Every couple of minutes it promotes the
convergences — "vessels went dark near reported activity", "spoofed tracks
co-located with an AIS gap" — into ranked incident cards with a written, cited
summary and a _slew to_ button. Actionable incidents are auto-promoted into the
investigation graph, so the ontology grows itself.

![Watch-officer intel brief: ranked, cited cross-domain incidents with slew-to](docs/media/ui-briefs.png)

**10. Keep it as evidence.** Anything worth keeping goes into the
chain-of-custody **evidence locker**: capture a URL snapshot, upload a file, grab
a screenshot, or freeze the current feed state, and each artifact is hashed with
SHA-256, written to `./data/evidence`, and logged to an append-only custody
chain in the local ontology store. Attach it to a situation, re-verify the hash
any time, or roll a whole case into a signed hash-of-hashes manifest. Fully
keyless. From there, **Reports** exports the case (see [Export](#export)) as a
JSON bundle, a self-contained HTML report with per-claim provenance footnotes,
or a PPTX brief — with any AI narrative confined to a clearly labelled block.

**11. A tool that draws on the map gets its own window.** The annotation
editor, the watchbox rules and the field kit open as free-floating, draggable,
resizable windows over the globe rather than stealing the rail from the layer
tree — arm a tool from the map toolbar, set up what it draws beside the thing
you are drawing on, and _Window → Re-dock every floating panel_ clears them all.
Positions persist for the session.

![Two floating windows over the live globe: the annotation editor with its shape, colour and style controls, and the watchbox panel with its enter/exit/loiter trigger rules](docs/media/ui-detach-toolbar.jpeg)

**12. Bring your own data — the Foundry tab.** Upload a CSV/JSON/NDJSON, shape it
through a governed pipeline (13 transform steps: filter, derive, join, aggregate,
union, window, pivot, dedup, cast, sort…), gate every version with data-health
checks (freshness SLAs, schema-drift, uniqueness…), and bind it into the same
ontology graph as the live feeds. Lineage, immutable versions with rollback, and
a dead-letter for rows that fail a transform all come along; the pipeline graph
flags exactly which outputs went stale.

![Foundry lineage pipeline: raw datasets on the left feeding five transforms into derived outputs, with raw, derived and stale colour-coded](docs/media/foundry-pipeline-new.png)

**13. Automate it — the Workflows tab.** A node-graph editor over the same live
feeds and ontology: wire sources (aircraft, vessels, quakes, alerts, datasets,
ontology, countries) through sandboxed Python/SQL/LLM transform blocks into sinks
(alerts, ontology objects, datasets, persistent memory), then run it on a
schedule. Control blocks can reach outward too — webhooks to your own server, or
a MAVLink bridge for drone tasking that rehearses log-only without a vehicle.

![A Workflows DAG: live aircraft through Python and SQL into an alert](docs/media/workflows-dag.png)

## Scope and limits

The point is fusion. Plenty of sites already do one feed well: Flightradar24 for
planes, MarineTraffic for ships, [GPSJam](https://gpsjam.org) for jamming.
Velocity goes after the seam between them: AIS plus radar imagery flags a ship
that's switched its transponder off; a cluster of aircraft reporting bad GPS
integrity becomes a jamming hotspot; when two or more of those line up in the
same place and time, they get promoted to a single incident with a written,
cited summary.

A few things worth knowing up front, because I'd rather you read them here than
be annoyed later:

- It's built for one analyst. One optional API key, no accounts or roles.
- **What persists vs what clears.** Durable under `./data/` or the Timescale
  service: the position-history archive (a Postgres + TimescaleDB hypertable by
  default; SQLite at `./data/history.db` remains the keyless fallback when no
  `HISTORY_PG_DSN` is set, with one boot warning naming the switch), the local
  ontology store (ontology objects, case files, situations;
  `intel/ontology_local.py`, `./data/ontology.db`), plus separate SQLite files
  for Foundry, Workflows,
  alert rules, cached news and the Instability Index scorer (`foundry.db`,
  `workflows.db`, `alert_rules.db`, `news_history.db`, `instability.db`), and a
  keyless-local audit log (`audit_log.db` — only materializes when no
  `SUPABASE_URL` is configured; a keyed deployment writes audit rows to
  Supabase instead and never creates the file). That's 8 SQLite files, not
  one — back up the whole `./data` volume, not a single DB. Also durable: the
  evidence locker (blobs under `./data/evidence`, custody chain in the
  ontology store's append-only `assertions` table), a disk tile cache
  (`./data/tilecache`) and any downloaded local models (`./data/models`).
  Volatile, and cleared on backend restart: the live incident list, transient
  AOI selections, and generated watch-officer briefs. The Docker Compose
  volumes mean the durable stores survive `docker compose down`, not just a
  process restart.
- **Retention, honestly.** The dev compose sets `HISTORY_RETENTION_HOURS=48`
  (2 days), but that's a ceiling, not a promise: `HISTORY_MAX_BYTES` (2 GB by
  default) binds first, and at measured global ADS-B+AIS ingest (~3 GB/h on a
  keyless box pulling full-breadth feeds) the byte cap fills well before the
  hour window matters, leaving well under 2 hours of effective depth out of
  the box — not 48. The hour window only becomes the binding limit once disk
  is large enough for it to be the tighter cap; a genuine 48h global archive
  needs on the order of 150 GB (`~3 GB/h * 48h`). Raise `HISTORY_MAX_BYTES`,
  or set `ARCHIVE_MODE=1` with `HISTORY_DISK_BUDGET_GB` sized to what you're
  willing to spend on disk (the production compose profile does this, 5 GB to
  start). `/api/history/coverage` reports the ACTUAL current depth
  (`oldest_ts`), not the configured ceiling, and the 2D console's day-picker
  and ownership chip render it, so you can see what you're really getting.
  Depth answers now come from the hourly `positions_hourly` aggregate; the old
  SQLite coverage scan took 73 s over 78M fixes, which is why the rollup
  exists.
- AIS runs keyless and global (~50k vessels tracked, MMSI-deduped across
  ShipXplorer, MyShipTracking, Digitraffic and Kystverket), but coverage is densest over
  Northern Europe and the Baltic and thins out elsewhere; an AISStream key fills
  in the gaps. Sparse regions still lean on the radar (SAR) layer.
- **The two headless-Chrome sidecars are degraded under Docker, not crashed.**
  ADS-B (OpenSky + the httpx tar1090 mirrors + ShipXplorer AIS) and the
  regional Kystverket/Digitraffic AIS feeds are direct HTTP calls and run at
  full strength in the container. But the tar1090 ADS-B sidecar and
  MyShipTracking (the enabled primary keyless _wide-area_ AIS source) drive a
  real Chromium via Node, which the slim API image doesn't ship — they log a
  warning and sit idle rather than crash the backend, so you lose their extra
  coverage, not the app. Run bare-metal (`bash scripts/run-api.sh`) for full
  sidecar coverage.
- Some **Analyze** apps are notional by design — the Targeting kill-chain board
  and the Video/FMV sensor HUD carry a "NOTIONAL // SIMULATED" banner and are
  driven by the Sim overlay, not a live weapons or ISR feed.
- The 3D satellite view will eat your VRAM. The default 2D dark map runs on a
  laptop; check [System requirements](#system-requirements) before switching the
  heavy mode on.

None of it needs an API key to start. Keys only add reach.

## What is enforced

Not promises, checks in the tree. Each line names the route or test that holds
it:

- **Clearance on reads and writes.** One predicate,
  `intel/ontology.visible_to`, filters local reads (`get`, `list_by_kind`,
  `search`, links, assertions) and the situations, maps and evidence routes by
  the caller's level and compartments; keyless callers are clearance 0, and a
  write above clearance or onto a hidden row answers 403.
  → `apps/api/tests/test_clearance_local_reads.py`
- **Custody-chained evidence.** Every capture is hashed with SHA-256 and
  appended to an immutable custody log; `GET /api/evidence/{sha}/verify`
  re-checks the bytes. → `apps/api/tests/test_evidence.py`
- **Hash-chained audit.** Mutations append to `action_log.db`; `GET
/api/audit/verify` walks the chain. → `apps/api/tests/test_asvs_fix_a_logging.py`
- **Citations withheld when ungrounded.** `LLM_REQUIRE_CITATIONS=1` (default)
  withholds a selection or country brief that cites none of its own ids;
  `GET /api/ai/guardrails` reports the posture. →
  `apps/api/tests/test_citations_hard.py`
- **Human-in-the-loop write-back.** A write-back action is operator-only and
  runs only from a proposal the operator approves; the audit row carries the
  payload's SHA-256, never the payload. → `apps/api/tests/test_writeback_action.py`

## What it pulls in

Live numbers measured off a running keyless backend on 2026-08-08; they move
around through the day:

| Feed                            | Typical live count                       | Where it comes from                                     |
| ------------------------------- | ---------------------------------------- | ------------------------------------------------------- |
| Aircraft (ADS-B)                | 12–15k (14,436 measured)                 | OpenSky + airplanes.live + adsb.lol + FR24              |
| Military aircraft               | ~30–60                                   | adsb.lol + airplanes.live                               |
| Vessels (AIS)                   | ~50k tracked, global                     | ShipXplorer + MyShipTracking + Digitraffic + Kystverket |
| GPS jamming                     | ~220 flagged 1° cells                    | ADS-B NACp/NIC, the GPSJam method                       |
| Dark vessels                    | radar change-detection                   | Sentinel-1 SAR                                          |
| Fused incidents                 | correlation-driven                       | the correlation engine                                  |
| Satellites                      | 16,095, SATCAT-enriched                  | CelesTrak                                               |
| Airspace (TFRs)                 | live restriction polygons                | FAA                                                     |
| Military bases                  | 7,183 (+1,330 MIRTA/Wikidata)            | Wikidata + DIA MIRTA                                    |
| Naval broadcast warnings        | ~800 plotted points                      | NGA NAVAREA                                             |
| Ports                           | 3,804, with harbour detail               | NGA World Port Index                                    |
| Airports                        | runways + ILS + frequencies + live METAR | FAA NASR, NOAA, LiveATC                                 |
| Earthquakes                     | ~290/day                                 | USGS + EMSC                                             |
| News + fact-check               | ~370 articles (off by default)           | publisher RSS                                           |
| Internet outages                | country level                            | IODA, Cloudflare                                        |
| Submarine cables                | ~700                                     | TeleGeography                                           |
| Conflict events                 | ~1.5k live                               | GDELT, EONET, ACLED                                     |
| Wildfires                       | VIIRS hotspots                           | NASA FIRMS (needs a key)                                |
| 3D war damage, imagery, webcams | varies                                   | Sentinel, GIBS, OSM                                     |
| Per-country toolkits            | 53 countries                             | open-data / registries / sanctions links                |

Optional keys, if you want more reach: AISStream for global AIS, an OpenSky
login for a bigger ADS-B budget, `FIRMS_MAP_KEY` for fires, an ACLED key for
conflict events, `CLOUDFLARE_TOKEN` for outages. `GET /api/intel/sources`
reports what's actually live versus what's still waiting on a key.

### Use your own receiver

If you run an ADS-B receiver (readsb, ultrafeeder, tar1090), Velocity folds its
aircraft into the same picture as the public feeds and records them to your
archive like everything else. Point `ADSB_FEED_URLS` in `.env` at your
tar1090's `aircraft.json`. The variable replaces the default list, so keep the
defaults in it if you still want them:

```bash
ADSB_FEED_URLS=http://192.168.1.20:8080/data/aircraft.json,https://globe.theairtraffic.com/data/aircraft.json,https://skylink.hpradar.com/data/aircraft.json,https://api.adsb.lol/v2/point/0/0/20000
```

Contacts are merged by ICAO address, freshest fix wins. Inside Docker,
`localhost` is the API container, so use the receiver's LAN address.

For a small desk display of what is overhead (ESP32, e-ink), `GET /api/overhead`
returns the nearest aircraft as compact JSON from the same snapshot, with no
upstream call of its own: see [docs/desk-display.md](docs/desk-display.md).

## MCP server: query the live console from an AI agent

The part I think is genuinely new: the backend doubles as a **Model Context
Protocol** server, so an AI agent can interrogate the same warm feeds the globe
renders without scraping a dozen sites or flooding its own context. Ask "where
is GPS being jammed right now?" and it answers from the live feed. Full
architecture + `/api/intel/*` HTTP reference: [`docs/mcp-server.md`](./docs/mcp-server.md).
It exposes 94 tools over `app.mcp_server` (a representative slice below; run
`--list-tools` for the full set):

| Tool                                  | What it returns                                                                                                                                                                                                       |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_situation`                       | Global summary: aircraft by category, GNSS-degraded count, emergencies, worst jamming cells, vessel/alert counts. The cheap first call.                                                                               |
| `focus_area(lat,lon,radius_nm)`       | **Loads a region PRIMARY** (dedicated fresh `/v2/point` fetch + ongoing priority refresh, independent of global rate limits) and returns a full bundle: aircraft + density + GPS jamming + vessels + fused anomalies. |
| `aircraft_density`                    | Grid of cells (count, by category, GNSS-degraded) for an area.                                                                                                                                                        |
| `gps_jamming`                         | GPSJam-method assessment (ADS-B NACp<8 / NIC<7, 1° bins): flagged cells, severity, affected aircraft. Global or scoped.                                                                                               |
| `query_aircraft`                      | Filtered query (bbox/centre, category, squawk, callsign, altitude band, emergency / gnss_degraded / on_ground).                                                                                                       |
| `lookup_aircraft(ident)`              | One aircraft by ICAO24 or callsign + integrity/threat assessment.                                                                                                                                                     |
| `query_vessels`                       | AIS vessels in an area, classified; `dark_only` for dark-vessel candidates.                                                                                                                                           |
| `anomalies`                           | Fused report: emergencies, jamming hotspots, dark vessels, alerts + a triage threat level.                                                                                                                            |
| `detect_deception` / `locate_emitter` | Flag spoofed/co-located tracks; triangulate a jamming source from degraded-integrity aircraft.                                                                                                                        |
| `whats_changed` / `incident_history`  | Delta since a timestamp; the incident timeline for an area.                                                                                                                                                           |
| `deep_analyze(question, lat?, lon?)`  | Gathers the relevant intel JSON and has a **reasoning model** reason over it (DeepSeek when configured, else a local Ollama model), so heavy analysis stays off the agent's context and only the conclusion returns.  |

Every tool returns compact, bounded JSON (counts, grids, ≤50-item samples), so
an agent can sweep the planet for a few hundred tokens instead of pulling 15k
features. Heavy tools also take **`detail='short'`** (the default digest — top-N
of each list plus a `<field>_total`) or **`detail='long'`** (the full bundle), so
an agent sweeps in `short` and drills in `long`. Area-primary loading means the
agent's region of interest stays fresh and dense even while the global firehose is
being rate-limited; the rest of the world keeps streaming from the sticky snapshot.

The same fusion powers the in-app **AI selection brief**: click an entity and
`POST /api/ai/selection/brief` fuses its registry enrichment and pattern-of-life
dossier into a selection-tier model prompt — every claim cites the dossier field
it came from, an uncited answer is withheld rather than shown, and it degrades
to the raw props if no model is configured.

### Install as a Claude Code plugin (skill + commands + agent)

The repo is also a Claude Code **plugin marketplace**. One install wires the MCP
server _plus_ an analyst skill (`osint-intel`), slash commands (`/osint-brief`,
`/osint-watch`, `/osint-jamming`), and a `osint-watch-officer` agent. Start the
backend (`bash scripts/run-api.sh`), then in Claude Code:

```
/plugin marketplace add /path/to/OSINT
/plugin install osint-geoint@osint-velocity
```

Set **repo_dir** and **python** (the repo's venv interpreter) when prompted; the
plugin runs that Python directly, so it works on Windows, macOS, and Linux. The
installer prints the exact commands for your OS: `bash
plugin/osint-geoint/install.sh` (Linux/macOS, `-y` to register the MCP server) or
`plugin\osint-geoint\install.ps1` (Windows, `-Run` to register).

### Point your agent at your own instance

The MCP server is mounted into the backend at `/mcp` (streamable-HTTP), so
once you're running Velocity yourself there's nothing extra to install to
query it from an agent. Register it with any MCP client against your own
host:

```bash
claude mcp add --transport http osint-geoint \
  http://localhost:8000/mcp \
  --header "Authorization: Bearer $VELOCITY_TOKEN"
```

`$VELOCITY_TOKEN` is only required if you've set `API_KEY` on the backend; a
keyless local instance needs no header at all.

### Self-host / develop

```bash
# 1. backend must be running (provides the warm feeds)
uv run --project apps/api uvicorn app.main:app --port 8000

# 2a. MCP server over stdio (Claude Code / Desktop / Agent SDK), cross-platform
uv run --project apps/api python -m app.mcp_server
# 2b. or streamable-HTTP
uv run --project apps/api python -m app.mcp_server --http --port 8765
# introspect (no backend needed)
uv run --project apps/api python -m app.mcp_server --list-tools
```

To register the local stdio server with Claude Code, run from the repo root:

```bash
claude mcp add osint-geoint -- uv run --project apps/api python -m app.mcp_server
```

`uv run` resolves the right interpreter on Linux, macOS, and Windows without
hardcoding a venv path. No `uv`? Point it at the venv Python directly:
`apps/api/.venv/bin/python -m app.mcp_server` (Linux/macOS) or
`apps\api\.venv\Scripts\python.exe -m app.mcp_server` (Windows), run from
`apps/api`.

Config (env or `apps/api/.env`): `API_BASE`, `API_KEY`, `OLLAMA_HOST`,
`OLLAMA_MODEL` (empty picks the smallest installed model; `deep_analyze`
degrades to returning raw JSON if Ollama is absent). The MCP server never
crashes a tool call: backend down returns a structured `backend_unreachable`
error; Ollama down falls back to raw intel JSON.

## Export

Two ways out, for two jobs:

**The live picture.**
`GET /api/export?fmt=geojson|csv|kml&kinds=aircraft,vessels&bbox=min_lon,min_lat,max_lon,max_lat&limit=N`
downloads the current snapshot the globe renders as **GeoJSON** (QGIS /
kepler.gl / Leaflet), **CSV** (spreadsheets), or **KML** (Google Earth). `bbox`
clips to a viewport; `kinds` is comma-separated (default `aircraft`); `limit`
caps features; vessels are best-effort.

**A finished case.**
`POST /api/situations/{id}/export?fmt=json|html|pptx` walks a situation's
children, its sourced assertions and every attached piece of evidence into a
shareable artifact: a self-describing **JSON** bundle, a self-contained **HTML**
report with a provenance footnote on each claim, or a **PPTX** brief. Any AI-written
narrative is confined to one labelled block — the rest is your cited evidence.

## System requirements

The heavy component is the **client**, a CesiumJS WebGL2 globe. It is GPU- and
browser-main-thread-bound, and the backend is light. WebGL2 is required
(Chrome/Edge 110+, Firefox 110+). On hybrid-graphics laptops, force the discrete
GPU (`chrome://gpu`, look for adapter "ACTIVE").

**VRAM depends heavily on which mode you run:**

- **2D-dark (default basemap):** light. The globe is a proxied 2D raster basemap
  plus the entity layers (aircraft/vessels). Runs on integrated graphics / ~2–4 GB
  VRAM, which is the right mode for modest hardware.
- **3D-sat (satellite imagery + world terrain + OSM 3D buildings, optional Google
  Photorealistic 3D):** **VRAM-heavy.** CesiumJS streams terrain meshes, high-res
  imagery, and 3D-tile building/photogrammetry sets, and it caches into whatever
  VRAM is available, measured at **20+ GB on a 32 GB card**. Tilesets are now
  individually cache-capped (Google 3D ~1.5 GB, OSM buildings ~0.5 GB) and MSAA is
  off (FXAA instead), but with terrain + global imagery + a high-DPI/4K canvas the
  resident set is still large. On a card with less VRAM, Cesium evicts and
  re-fetches more aggressively (lower fps, more pop-in) and still runs.

| Tier          | GPU                                              | RAM   | Display     | What you get                                                                                                                                                             |
| ------------- | ------------------------------------------------ | ----- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Edge / lite   | WebGL2 integrated (Iris Xe / Vega / M1)          | 8 GB  | 1080p       | `OSINT_PROFILE=lite` (default). 9k aircraft, 32k vessels, 2D-dark. Backend fits in ~1.5 GB; the adaptive governor sheds low-priority layers to keep frames under budget. |
| Recommended   | Discrete ≥8 GB VRAM (RTX 3060 / RX 6700 / M-Pro) | 16 GB | 1080p–1440p | Full profile with all feeds. 2D-dark smooth; 3D-sat usable at city scale.                                                                                                |
| Full / 3D-sat | RTX 4070+/16 GB VRAM or better                   | 32 GB | up to 4K    | All feeds + browser sidecars + 3D-sat terrain + buildings; best frame rate.                                                                                              |

These tiers come from a cgroup-isolated benchmark (`tools/perf/bench_ram.sh`)
that constrains backend + Vite + Chrome inside a single MemoryMax and measures
real fps, renderMs, entity counts, and OOM kills at each RAM level. 3D-sat
genuinely wants a lot of VRAM, and the low-VRAM minimum only holds for the
2D-dark map; switch on 3D-sat and you'll want a discrete card with headroom.

**Backend (server):** Python 3.12, outbound HTTPS. The `lite` profile
(default) disables browser sidecars, history, and the FR24/OpenSky-gaps tiers,
measuring ~530 MB steady-state RSS — comfortably fits an 8 GB box alongside
the browser. The `full` profile with all direct feeds warm measures ~3.2 GB.
The optional headless-Chrome coverage sidecars are the real memory line item:
the ADS-B scraping tier measured ~11 GB RSS across its ~50-process Chrome tree
and the AIS tier ~3-6 GB across its ~20-36 (both measured 2026-07-24 at full
coverage), so budget roughly 15 GB on top of the backend when both are
enabled — or keep `lite` and use the direct feeds.

## Stack

- **Frontend**: Vite + React 18 + TypeScript + CesiumJS + MapLibre GL JS v5.24 + Tailwind + Zustand
- **Backend**: FastAPI (Python 3.12) + httpx + websockets. Live _derived_ state
  is in-process (bounded observation store + disk tile cache); durable state
  (the position-track replay archive, the ontology store: objects / case files /
  situations, and the evidence locker's custody chain) persists to
  Postgres + TimescaleDB or SQLite, on Docker volumes in Compose, with a
  rolling window by default or an open-ended disk-budgeted archive via
  `ARCHIVE_MODE`
- **Agent access**: Model Context Protocol server (`app.mcp_server`, MCP SDK) + optional local Ollama analysis
- **Data**: PostgreSQL 16 + TimescaleDB backs the position archive today (6 hour
  chunks, columnar compression, hourly `positions_hourly` rollup, SQLite as the
  keyless fallback). PostGIS + Redis remain planned.
- **Infra**: Docker Compose, nginx reverse proxy

## Layout

```
osint/
├── apps/web/                 # React + Cesium console (14-app workspace)
│   └── src/
│       ├── globe/            # Cesium globe, layers, map toolbar (measure/area/annotate)
│       ├── entity-panel/     # right-rail dossier / inspector
│       ├── evidence/         # chain-of-custody evidence locker UI
│       ├── foundry/          # FOUNDRY surface (datasets, pipeline DAG, builds, ontology)
│       └── shell/            # app switcher + detachable floating panels
├── apps/api/                 # FastAPI backend
│   └── app/
│       ├── intel/            # agent-facing analytics, local ontology store, evidence, case export
│       ├── foundry/          # BYO-data layer: datasets, transforms, builds, checks, binding
│       ├── routes/intel.py   # /api/intel/* deep-query JSON API
│       ├── routes/evidence.py# /api/evidence/* capture / verify / attach / manifest
│       ├── routes/foundry.py # /api/foundry/* datasets/pipelines/checks/bindings
│       └── mcp_server.py     # Model Context Protocol server
├── packages/shared/          # Shared TS types (LayerDescriptor, Observation)
├── docs/                     # design notes, decisions.md, foundry-plan.md
└── infra/                    # Docker, nginx, db init
```

## Tests

```bash
# from the repo ROOT (running from apps/api makes .env auth resolve → a wall of 401s)
OSINT_DISABLE_BACKGROUND=1 apps/api/.venv/bin/pytest apps/api -q   # 3276 passed + 21 skipped
pnpm -r test                          # vitest (web, shared)
pnpm -r typecheck
bash scripts/verify.sh                # typecheck + lint + web unit + api tests in one shot
# manual MCP integration drivers (need backend on :8000):
#   apps/api/.venv/bin/python tests/mcp_client_check.py   # stdio handshake
#   apps/api/.venv/bin/python tests/mcp_full_check.py     # tools end-to-end + Ollama
```

## Phase status

This is the internal build log, in order shipped — not the pitch (that's
above). Legend: ✅ shipped · 🚧 in progress

- ✅ **Phase 0** — Foundation
- ✅ **Phase 1** — MVP: live ADS-B / AIS / quakes / GPS-jamming layers on the globe
- ✅ **Phase 2**: Replay + drill-in. The timeline scrubber and the history
  archive ship with a Postgres + TimescaleDB backend (48h rolling window by
  default, or an open-ended disk-budgeted archive with `ARCHIVE_MODE`), SQLite
  as the keyless fallback, and a tar1090-style half-hour chunk replay pipeline
  that carries aircraft back to 2024 from public readsb heatmap archives;
  drilling into any past moment works today. PostGIS + Redis for the rest of
  the platform's state are a deferred scaling upgrade, not a blocker
- ✅ **Phase 3** — Fusion engine + alerts (correlation rules) + 2D mirror
- 🚧 **Phase 4** — Advanced sensors + agent access. MCP server + intel API
  (now with a Claude Code plugin and `detail=short|long` tool variants), Sentinel-1 SAR
  dark-vessel detection, an autonomous watch-officer that writes cited incident
  briefs and auto-populates the ontology graph from them, a keyless
  infra/domain OSINT layer, a places/airspace layer (FAA TFR polygons,
  military bases, NGA naval warnings, World Port Index port detail, NASR/METAR
  airport enrichment, an 8-way keyless basemap picker), a **chain-of-custody
  evidence locker** and **case→report export** (JSON/HTML/PPTX with per-claim
  provenance), a globe map toolbar (measure/area-select/annotate) and detachable
  floating panels, a photo-geolocation pipeline, a City 3D Gaussian-splat viewer,
  optional local-GPU (Ollama) inference, and a first-run onboarding tour. More
  sensors and deeper analysis are ongoing.
- 🚧 **Phase 5** — Foundry: a keyless, local, single-operator data-integration
  loop. Upload → transform (governed 13-step DSL with
  lineage) → build (dependency DAG, staleness, cycle rejection) → data-health
  checks → bind into the local ontology graph. In: immutable versions +
  rollback, row-level quarantine/dead-letter, freshness/schema-drift SLAs,
  window/pivot analytics, entity resolution. Deliberately out of scope
  (single-operator identity): multi-tenant MLS, distributed compute, streaming
  CDC, connector catalogs. Next: ontology Actions (audited write-back) and
  dataset branches.
- 🚧 **Phase 6** — Workflows: a node-graph automation layer over the same live
  feeds and ontology. 20 blocks — sources (aircraft, vessels, quakes, alerts,
  datasets, ontology, countries), transforms (`op.python`/`op.sql`/`op.llm`
  sandboxed subprocesses, `op.geo`, `op.http`, `op.steps`), sinks (alert,
  ontology, dataset, persistent memory), and **external-actuation control
  blocks** that reach out of the platform: `control.webhook` to any server, and
  `control.drone`/`control.device` to command a UAV or hardware via a JSON
  envelope. A first-class **MAVLink bridge** (`app.mavlink_bridge`,
  ArduPilot/PX4, log-only without a vehicle so you can rehearse) ships in-repo.
  See [`docs/workflows-control-blocks.md`](./docs/workflows-control-blocks.md).

See [`docs/`](./docs) for the per-feature design notes and pipeline writeups.

## License

[AGPL-3.0-or-later](./LICENSE) covers Velocity's **source code**. Upstream **data
carries its own licenses**; several feeds are non-commercial / academic (e.g.
ACLED, adsb.fi, OpenSky). See [`NOTICE`](./NOTICE) for per-source attribution and
terms, and verify each upstream's current terms before any commercial or
redistributive use.

No warranty, and no liability for how you collect or use the data — see
[`DISCLAIMER.md`](./DISCLAIMER.md), which also covers scraping, upstream terms,
personal data, and prohibited uses.

TDQS

C2.8/5.0

Scored across 94 tools

Disambiguation2/5

The tool set contains many overlapping analytic entry points: get_situation, focus_area, anomalies, intel_brief, area_baseline, whats_changed, deep_analyze, and answer all return situation/intel conclusions with subtle boundaries. Aircraft and vessel access is also split across several near-equivalent tools (query_aircraft/lookup_aircraft/adsb_by_*/fr24_search; query_vessels/maritime_vessels_bbox/maritime_satellite_positions), making misselection likely.

Naming Consistency3/5

Almost all names are lowercase snake_case, which is consistent at the formatting level. However, the naming convention itself is mixed: some tools use verb_noun (query_aircraft, create_watch_rule, detect_deception), while many are bare nouns or domain labels (anomalies, intel_brief, gps_jamming, tinygs_packets, weather_sondes).

Tool Count1/5

94 tools is an extreme surface for a single MCP server, far beyond the 3-15 well-scoped range. While the platform covers many data domains, the count is so large that agents must navigate a sprawling catalogue rather than a focused tool set.

Completeness4/5

The surface covers an unusually broad OSINT/geospatial domain: situation awareness, aircraft/vessel queries, anomalies, history, watch rules, proposals, imagery, disaster feeds, cyber feeds, and many specialist sources. Minor lifecycle gaps exist, such as no update_watch_rule, but core workflows and many optional layers are represented.

Maintenance

ActivityNo data
ResponsivenessOutdated data