Back to Projects

// Software & Data · March 2026

Crude Flow

Next.jsTypeScriptMapbox GL JSWebSocketsServer-Sent EventsReal-Time SystemsData Visualisation

A command-centre map of live tanker traffic, with the chokepoints, conflict zones and news that move the oil price layered on top.

The Problem

AIS doesn't tell you what a ship is and where it is in the same message. Position reports arrive continuously and carry no vessel type; the static data identifying a ship as a tanker arrives separately and irregularly. So filtering a global feed down to oil tankers means you've already drawn a vessel on the map by the time you learn it's a car ferry — and the raw feed is far more traffic than a browser can turn into map updates anyway.

The Approach

Held the identity problem on the server. A Next.js route opens the WebSocket to aisstream.io and bridges it to the browser as Server-Sent Events, which keeps the API key off the client and gives somewhere stateful to reconcile the two message types. The server maintains an MMSI-keyed cache of static data, enriches each position report as it passes through, and — when static data proves a vessel isn't a tanker — emits an explicit remove message so the client can retract something it has already rendered. The cache is capped at 10,000 vessels with oldest-first eviction so a long-running stream can't grow without bound. On the client, incoming positions buffer into a Map and flush to the Mapbox GeoJSON source every five seconds rather than per message, with static fields preserved across merges so a sparse update doesn't blank a vessel's destination. Mapbox clusters below zoom 6, and the dashboard is dynamically imported with SSR off to keep the map library out of first paint.

The Outcome

Runs against five live data sources at once — AIS positions, Mapbox tiles, ACLED conflict events, oil prices and news feeds — with tankers updating on the map every five seconds and chokepoint traffic counted from live vessel positions. The cache intervals weren't a design preference but a constraint: free tiers of 800 and 100 requests a day set prices to five minutes, news to ten, and conflict data to an hour. The outstanding work is hosting — the AIS bridge holds a long-lived stream, which doesn't fit inside a serverless function's execution limit, so that one route needs an always-on host.

View on GitHub