Repository navigation
Releases: AndrewCTF/velocity
Release list
v1.2.0
Minor release. The console now works on a phone, a replay can leave the desk as a link, the archive can live in TimescaleDB, and the globe is a lot faster.
Read "Upgrading" before you pull if you run docker-compose.prod.yml.
Replay any day, and share it
- Public-archive replay. Aircraft replay from the public readsb heatmap archives for any day since 2024, before your own archive starts. The Play button drives the clock through half-hour chunks with a 24-entry cache, decoded in a Web Worker.
- Rewind links. A URL opens the map at a place, a contact and a moment.
- TimescaleDB archive. With
HISTORY_PG_DSNset (both compose files now set it), the position archive lives in a compressed Postgres + TimescaleDB hypertable with an hourly rollup. Without it, the backend falls back to SQLite. - Projections.
GET /api/history/projectreturns a one-hour cone and chokepoint ETAs from a contact's own track.
On your phone
- At 767 px and under, the console is a map app: the map takes 93% of the screen, and Layers, Find, Time, Alerts and More open as bottom sheets you can drag.
- Area alerts push to your phone through ntfy, with "Alert me about this area" on the map.
/api/overheadreturns the nearest aircraft as small JSON for desk displays.- arm64 images are published next to amd64.
Faster globe
Measured A/B on the same backend, headed Chrome on the GPU:
| v1.1.0 | v1.2.0 | |
|---|---|---|
| all layers on, fps p05 / p50 | 9 / 50 | 51 / 91 |
| fly-in to 400 km, worst frame | 340 ms | 94 ms |
| boot chunk (gzip) | 746 KB | 434 KB |
Ships
A new AIS pipeline polls ShipXplorer, an OpenWaters sweep and VesselFinder into a freshness-wins store keyed by MMSI, so a cached source can never overwrite a fresher fix.
MCP server
focus_area takes a polygon (one ring of [lon, lat] pairs) as well as a circle. The server is also listed on Glama: https://glama.ai/mcp/servers/AndrewCTF/velocity
Analyst features
- Clearance is enforced on every ontology read and write through one predicate.
- Entity-resolution merge candidates go to a review queue; nothing auto-merges.
- Foundry pulls a named SQL table by cursor and ingests documents (eml, docx, txt, md, pdf).
- Write-back actions go through the human-in-the-loop proposal queue.
Google Earth 3D basemap (off by default)
A photoreal city mesh with no API key. It reads a private Google endpoint, outside Google's terms, so it stays off unless you set GOOGLE_3D_KEYLESS=true. The licensed ENABLE_GOOGLE_3D stream never turns it on.
Security
Dependency advisories closed in vite, dompurify, vitest, pillow (13.0.0) and certifi. pnpm audit and pip-audit report no known vulnerabilities.
Upgrading
docker-compose.prod.ymlnow runs a TimescaleDB service and refuses to start withoutTIMESCALE_PASSWORDin.env. It still requiresVELOCITY_VERSION; set it tov1.2.0.- The dev
docker-compose.ymlalso starts a TimescaleDB service now and points the API at it. - Either way, an existing SQLite archive is not copied automatically. To keep it, run
scripts/history_migrate_sqlite_to_pg.pyonce against the new database.
Full changes: #93, #95, #96, #97, #98, #99, #100, #101, #102.
Maps from the archive, October 2026
Full-resolution versions of the density maps rendered from Velocity's own recorded history. Not a software release.
- norway-ship-traffic-print.jpg: 3.4M AIS positions along the Norwegian coast over 12 days (Kystverket open AIS), 4918x5535.
v1.1.0 — hardened, keyless again, and gated on a booted stack
Minor release. It closes the security gaps found against ISO 27001, NIST CSF 2.0,
SSDF and ASVS 5.0, restores a basemap that works with no key, and stops images
from being published unless the real stack boots and renders.
Read "Upgrading" before you pull: several defaults now fail closed.
The globe works without a key again
CARTO began stamping "API KEY REQUIRED" onto every keyless Dark Matter tile,
server-side requests included, so v1.0.x globes showed the watermark. The default
basemap is now Esri's World Dark Gray canvas, with its label layer composited on
the server and cached as before.
- If the tile proxy can't be reached (backend down or still booting), the browser
fetches Esri base tiles directly, so the globe is never blank. Those fallback
tiles have no labels. - The map no longer waits on
/api/config. It renders on keyless defaults with a
"connecting to backend…" label, then upgrades in place. - The sign-in prompt appears only after the backend actually answers 401, not
whenever the backend is unreachable.
Trade-off: Esri's canvas stops at zoom 16 (CARTO went to 20), so street level is
upsampled. The satellite basemap still reaches 19.
Colour schemes
The seven hand-rolled schemes now use published palettes: IBM Carbon (default,
Daylight), GitHub Primer (Midnight, High contrast) and Radix Colors (Slate,
Night watch, Paper). The four Blueprint schemes are unchanged. Every scheme clears
WCAG AA in the contrast guard.
Security
The full gap analysis is docs/security/gap-analysis-2026-09.md, and all 253
ASVS 5.0 Level 1 + 2 requirements are assessed in
docs/security/asvs-l2-assessment.md. Highlights:
op.pythonruns in a bubblewrap jail and refuses to run without one.- Running a workflow or managing a model requires operator authority.
- The rate limiter no longer trusts a caller's own
X-Forwarded-For, and
X-Velocity-Tieris ignored from every peer. - Foundry is closed on a keyless box, and
/mcpanswers 503 untilAPI_KEYis set. - Baseline security headers, and a CSP in every production web build.
- The audit log is append-only and hash-chained (
GET /api/audit/verify). - Supabase sessions are verified against JWKS and re-checked with GoTrue, failing
closed. MFA and a maximum session age are opt-in. - Sidecars require a per-spawn bearer token.
Releases are gated
publish.yml now pushes images only after ci.yml and the new
release-gate.yml pass for the tagged commit. The gate builds the shipped
images, boots them with docker-compose.prod.yml, and drives the page in a real
browser. That includes loading with the API stopped and recovering in place when
it starts. The Cloudflare gateway and deploy.sh are gone; releases ship as
ghcr images only.
Upgrading
docker-compose.prod.ymlrequiresVELOCITY_VERSION(there is no
:latest):VELOCITY_VERSION=v1.1.0 docker compose -f docker-compose.prod.yml pull VELOCITY_VERSION=v1.1.0 docker compose -f docker-compose.prod.yml up -d
- nginx binds
127.0.0.1:8080. Put a TLS reverse proxy on the host in front
of it. ?key=works on WebSocket upgrades only. HTTP callers must send
X-API-KeyorAuthorization: Bearer.- Secrets shorter than 32 characters refuse to boot.
- Alert-rule webhooks must be public hosts unless listed in
WORKFLOWS_HTTP_ALLOW_HOSTS. op.pythonreturns 503 where the kernel blocks unprivileged user
namespaces, including the production container. Set
WORKFLOWS_PYTHON_UNSANDBOXED=1only if you accept running it unjailed.- The first tile requests after upgrade fetch from Esri. Old CARTO tiles stay in
the cache directory but are no longer served.
The operator-facing list is in docs/decisions.md and
docs/security/operator-hardening.md.
Images
ghcr.io/andrewctf/velocity-api:v1.1.0ghcr.io/andrewctf/velocity-web:v1.1.0
Full changelog: v1.0.1...v1.1.0
v1.0.1 — bound the archive's write-ahead log
Patch release. If you are running v1.0.0, upgrade — it ships an archive that
grows without bound.
The fix
v1.0.0's replay bar asked the backend for archive coverage every 5 seconds, and
each request scanned the whole archive. That scan takes longer than the gap
between polls, so a read transaction was always open — and SQLite can only
checkpoint its write-ahead log past the oldest live reader. The recorder never
stops writing, so the log grew forever.
It compounds, which is why it runs away rather than settling: every page read has
to search the log for that page's newest version, so a bigger log makes the scan
slower, which holds the reader open longer, which grows the log faster.
On the development box that produced a 49.6 GB write-ahead log against 15.2 GB
of actual data — 71 GB of disk for 15 GB of archive. A checkpoint reclaimed
48.6 GB of it, so 98% was redundant page versions that could never be written
back. Left recording on a smaller disk, it fills it.
Coverage is now cached behind a lock, so the scan runs at most once every two
minutes and never twice at once, and the log is bounded by journal_size_limit
so it is truncated back whatever the read pattern does.
Replaying the exact workload that caused it, against a real 16 GB archive:
| v1.0.0 | v1.0.1 | |
|---|---|---|
| write-ahead log | grew to 49.6 GB | 0 MB, flat |
| memory | +4-5 GB/min, peaked at 83 GB | flat, ~660 MB |
| coverage request | 73 s, every call | 14.8 s once, then cached |
Upgrading
Nothing to migrate. Pull and restart; the oversized log from a v1.0.0 run is
checkpointed back into the database on the next clean start. If you want the
disk back immediately, stop the API and run:
sqlite3 data/history.db "PRAGMA wal_checkpoint(TRUNCATE);"That took 37.7 s against a 49.6 GB log here. It is lossless — the log's contents
are written into the database, which is what a clean shutdown does anyway.
Everything else
Unchanged from v1.0.0. 1,721 backend tests passing. AGPL-3.0.
v1.0.0 — the open live world, recorded, and yours
Warning
Superseded by v1.0.1 — do not run this tag.
It records an archive whose write-ahead log grows without bound (49.6 GB against
15.2 GB of data on the box it was found on). Left recording, it fills the disk.
Fixed in v1.0.1; nothing to migrate.
Velocity is a self-hosted situation console. It pulls live aircraft, ships,
satellites, earthquakes, GPS jamming, dark vessels, restricted airspace, naval
warnings, outages and conflict events onto one 3D globe, correlates them on the
server, and keeps the position history on your own disk for as long as you give
it room.
This is the first tagged release. The project has been running daily against
live feeds since June; v1.0.0 marks the point where a stranger can clone it,
run one command, and get the same picture without asking anyone for a key.
Why this exists
Commercial trackers gate the part that matters. Flightradar24 gates replay at 7
days on the free tier, MarineTraffic cut its free history window to 24 hours,
and ADS-B Exchange killed its free API tier outright. A self-hosted tool does
not have that problem: turn on the archive profile and Velocity records position
history to your disk until the disk budget you set runs out. Nobody can
retroactively shorten your window.
What's in the box
- Live fusion globe — a snapshot measured at 12,647 aircraft (0.5 s old) and
57,420 vessels while writing this, the vessels MMSI-deduped across four
keyless sources. About 32,000 of those vessels are moored or parked and
long-retained, so the moving picture is nearer 25,000. Satellites are
propagated client-side with real SGP4, not faked motion. Category icons and
per-entity dossiers, not dots. - Replay you own — a disk-backed SQLite archive with a timeline scrubber.
48-hour rolling window by default, or an open-ended disk-budgeted archive. - An investigation layer — a local ontology (objects, assertions with
provenance, situations, case files), an evidence locker that hashes at
capture, and case-to-report export. - A watch officer — an autonomous pass over the live picture that promotes
correlated events into incidents with cited reasoning. - Workflows and Foundry — bring your own datasets, transforms and builds;
wire live feeds through Python and SQL blocks into alerts. - Keyless alerting — geofence and watch-list rules firing to Discord or a
generic webhook, server-side, with no browser open. - An MCP server — query the live console from an AI agent.
Keyless by default
ADS-B grid, Baltic AIS, MyShipTracking, ShipXplorer, USGS earthquakes, the
Carto basemap and CelesTrak all work with no API key. FIRMS degrades gracefully
without one. Keys are optional everywhere they appear.
Quick start
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 :8080Scope and limits
Read these before you get excited. It is a single-analyst tool, not a team
platform. Live derived state (the current incident list, AOI selections,
watch-officer briefs) lives in memory and clears on restart; what you
deliberately keep is durable — the position archive, the local ontology, and
the evidence locker. There is no facial recognition and there never will be.
AI-drafted prose is labeled as such wherever it reaches a document.
1,719 backend tests pass at this tag, most of them pinning hard-won invariants
about upstream feed behaviour that each regressed at least once.
AGPL-3.0.