How to Integrate Live Aircraft Data from the OpenSky Network into CesiumJS

You integrate live aircraft data into CesiumJS by proxying the OpenSky Network API through a Vite dev server to handle authentication and rate limits, then polling that endpoint to populate a BillboardCollection with position-updated billboards that use CallbackProperty for dead reckoning between updates.

The God's Eye View (GEV) repository provides a production-ready pipeline that streams live aircraft positions from the OpenSky Network into a CesiumJS viewer. By combining a Node.js proxy layer with Cesium's primitive collections and callback-driven entity updates, you can render thousands of moving aircraft with smooth interpolation and automatic fallback handling.

Architecture Overview

The integration relies on three tightly-coupled components defined in the GEV codebase:

  • Vite dev-server proxy (vite.config.js, lines 3017–3240): Exposes /api/opensky and handles authentication, caching, adaptive TTL, and 429 cooldown logic.
  • Flight-layer module (src/data/flights.js): Fetches the proxy endpoint, converts OpenSky rows into Cesium billboard entities, and drives dead-reckoning between updates.
  • Cesium viewer glue (src/data/flights.js lines 6–16 and src/worldFocus.js lines 71–79): Creates a BillboardCollection, updates the collection each poll, and attaches click-to-track callbacks.

Configure OpenSky Authentication

The proxy supports four auth modes: oauth, basic, auto, and anon. For development, use auto mode, which attempts OAuth first and falls back to HTTP Basic. Create a .env file in your project root (see .env.example in the repository):

OPENSKY_AUTH_MODE=auto
OPENSKY_CLIENT_ID=YOUR_CLIENT_ID
OPENSKY_CLIENT_SECRET=YOUR_SECRET
OPENSKY_USERNAME=YOUR_USERNAME
OPENSKY_PASSWORD=YOUR_PASSWORD

When the Vite dev server starts, vite.config.js reads these variables before any other code runs (see the provider initialization block around line 92). The proxy uses them to obtain a bearer token via getOpenSkyToken or to construct a Basic Authorization header (lines 80–110).

Proxy the OpenSky API with Vite

The OpenSky proxy is registered via openSkyProxy() inside your vite.config.js (lines 3017–3240). This intermediary handles all communication with https://opensky-network.org/api/states/all, keeping credentials server-side and bypassing CORS.

Key implementation features include:

  • Auth mode selection: normalizeOpenSkyAuthMode determines which credentials to send (lines 19–31).
  • Cache and adaptive TTL: A base 9-second cache (OPENSKY_CACHE_MS) stretches dynamically based on remaining daily credit budget via openskyAdaptiveTtlMs (lines 60–66).
  • Rate-limit handling: On a 429 response, the proxy records a cooldown (_openskyCooldownUntil, lines 151–160) and serves the last-good snapshot (lines 162–173).
  • Regional fallback: If the snapshot exceeds OPENSKY_SOURCE_STALE_MS (120 seconds), the proxy falls back to the free adsb.lol service via serveAdsbLolPointFallback (lines 27–35).

Poll and Parse Aircraft States

The flight layer (src/data/flights.js) creates a periodic poll (approximately every 30 seconds) that calls /api/opensky. The response is validated by _isUsableOpenSkyState (line 3216), which filters malformed rows and applies the on_ground flag logic (lines 1983–2000). Valid entries are stored in a Map keyed by ICAO24 (_flightData).

// src/data/flights.js – simplified polling logic
async function pollOpenSky() {
  const resp = await fetch('/api/opensky');
  const body = await resp.json();
  
  for (const st of body.states) {
    if (!_isUsableOpenSkyState(st)) continue;
    const [icao24, ...rest] = st;
    _flightData.set(icao24, {
      position: Cesium.Cartesian3.fromDegrees(rest[5], rest[6], rest[7] ?? 0),
      onGround: rest[1],
      trueTrack: rest[10]
    });
  }
}

Render Billboards in CesiumJS

After parsing, the module calls applyAircraftBillboardTreatment (lines 84–85) to convert each entry into a Cesium billboard. Billboards are stored in a single BillboardCollection created at module load time (line 6). Each billboard’s alignedAxis is set to the WGS84 surface normal so that the true_track heading rotates correctly relative to the ellipsoid (lines 8–10).

import * as Cesium from 'cesium';

const viewer = new Cesium.Viewer('cesiumContainer');
const flightBillboards = viewer.scene.primitives.add(
  new Cesium.BillboardCollection()
);

// Inside pollOpenSky(), after parsing each aircraft:
flightBillboards.add({
  position: aircraft.position,
  image: aircraftIcon,
  alignedAxis: Cesium.Ellipsoid.WGS84.surfaceNormal(aircraft.position, new Cesium.Cartesian3()),
  rotation: Cesium.Math.toRadians(aircraft.trueTrack ?? 0),
  color: isMilitaryIcao(aircraft.icao24) 
    ? Cesium.Color.fromCssColorString('#FFB800') 
    : Cesium.Color.WHITE
});

Add Click-to-Track and Dead Reckoning

To enable selection, the layer hooks a click-to-track gesture via bindTrackingClickGesture (line 31). When a billboard is clicked, the code creates a tracked Entity with a CallbackProperty (lines 12–16) that interpolates position between API refreshes:

function createTrackedEntity(aircraft) {
  const callback = new Cesium.CallbackProperty((time, result) => {
    // Dead-reckon forward using last velocity and heading
    return deadReckon(aircraft, time);
  }, false);
  
  return viewer.entities.add({
    position: callback,
    billboard: { image: TRACKED_ICON_PX, verticalOrigin: Cesium.VerticalOrigin.BOTTOM }
  });
}

Camera handling is encapsulated in src/worldFocus.js (lines 71–79), which uses Cesium’s BoundingSphere and HeadingPitchRange utilities to smoothly fly to selected aircraft.

Handle Rate Limits and Fallbacks

The OpenSky Network limits authenticated users to approximately 4,000 credits per day. The proxy implements three survival mechanisms:

  1. Adaptive TTL: Cache lifetime grows as daily credits deplete (openskyAdaptiveTtlMs).
  2. 429 Cooldown: Respects X-Rate-Limit-Retry-After-Seconds headers and serves stale data during the penalty window.
  3. adsb.lol Fallback: Switches to the free regional feed if the primary cache exceeds 120 seconds of staleness.

These mechanisms keep the Cesium layer alive even when the upstream API throttles requests.

Complete Integration Example

To replicate this pipeline in your own project:

  1. Copy the proxy logic from vite.config.js (lines 3017–3240) into your Vite configuration.
  2. Create a flight module that polls /api/opensky, stores data in a Map, and updates a BillboardCollection.
  3. Configure environment variables as shown in the authentication section.
  4. Start the dev server (npm run dev). The proxy serves live data while handling auth, caching, and rate-limit governance automatically.
// vite.config.js
export default defineConfig({
  plugins: [cesium()],
  server: {
    configureServer(server) {
      openSkyProxy().configureServer(server);
    }
  }
});
// Main application loop
const viewer = new Cesium.Viewer('cesiumContainer');
const flightData = new Map();

setInterval(async () => {
  const resp = await fetch('/api/opensky');
  const { states = [] } = await resp.json();
  // Update flightData and BillboardCollection...
}, 30_000);

Summary

  • Proxy required: Always route OpenSky requests through a server-side proxy (Vite in this case) to manage authentication and avoid CORS.
  • Use BillboardCollection: For performance, store aircraft icons in a single primitive collection rather than individual entities.
  • Align to surface: Set alignedAxis to the WGS84 surface normal to ensure heading rotations follow the ellipsoid.
  • Dead reckon: Use CallbackProperty to interpolate positions between the 30-second API polls for smooth animation.
  • Respect limits: Implement adaptive caching and fallback services (like adsb.lol) to stay within OpenSky's 4,000 credit daily quota.

Frequently Asked Questions

How do I handle OpenSky Network rate limits in CesiumJS?

Implement a server-side cache with adaptive TTL that stretches expiration times as your daily credits deplete. When you receive a 429 response, respect the Retry-After header and serve the last valid snapshot until the cooldown expires. For extended outages, fall back to alternative sources like adsb.lol.

Why use a Vite proxy instead of calling OpenSky directly from the browser?

The Vite proxy keeps your OpenSky credentials server-side, eliminating CORS restrictions and preventing API keys from leaking to clients. It also centralizes rate-limit logic, allowing the Cesium frontend to consume a simple /api/opensky endpoint without handling authentication flows or retry logic.

What is dead reckoning in the context of CesiumJS aircraft tracking?

Dead reckoning predicts an aircraft's position between API updates using last known velocity and heading. In GEV, this is implemented via a CallbackProperty attached to a tracked entity's position, which calculates interpolated coordinates every frame until the next OpenSky poll arrives.

Can I display aircraft without OpenSky credentials?

Yes, but with limited reliability. The GEV proxy supports an anon auth mode and falls back to adsb.lol for regional data when the OpenSky cache is stale. However, for global coverage and higher refresh rates, you should obtain OpenSky OAuth or Basic credentials and configure them in your .env file.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →