Skip to content

Releases: AndrewCTF/velocity

v1.2.0

Choose a tag to compare

@AndrewCTF AndrewCTF released this 22 Oct 04:33
3ca6f03

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_DSN set (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/project returns 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/overhead returns 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.yml now runs a TimescaleDB service and refuses to start without TIMESCALE_PASSWORD in .env. It still requires VELOCITY_VERSION; set it to v1.2.0.
  • The dev docker-compose.yml also 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.py once against the new database.

Full changes: #93, #95, #96, #97, #98, #99, #100, #101, #102.

Maps from the archive, October 2026

Pre-release

Choose a tag to compare

@AndrewCTF AndrewCTF released this 22 Oct 11:18
6571794

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

Choose a tag to compare

@AndrewCTF AndrewCTF released this 13 Sep 13:42
fe155b8

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.python runs 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-Tier is ignored from every peer.
  • Foundry is closed on a keyless box, and /mcp answers 503 until API_KEY is 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.yml requires VELOCITY_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-Key or Authorization: Bearer.
  • Secrets shorter than 32 characters refuse to boot.
  • Alert-rule webhooks must be public hosts unless listed in
    WORKFLOWS_HTTP_ALLOW_HOSTS.
  • op.python returns 503 where the kernel blocks unprivileged user
    namespaces, including the production container. Set
    WORKFLOWS_PYTHON_UNSANDBOXED=1 only 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.0
  • ghcr.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

Choose a tag to compare

@AndrewCTF AndrewCTF released this 16 Jul 13:06
175297b

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

Choose a tag to compare

@AndrewCTF AndrewCTF released this 16 Jul 11:44
db86a57

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 :8080

Scope 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.