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

> Integrate live aircraft data from OpenSky Network into CesiumJS. Learn to proxy API requests, poll for updates, and use CallbackProperty for smooth, real-time visualizations in your application.

- Repository: [Bilawal Sidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view)
- Tags: how-to-guide
- Published: 2026-09-04

---

**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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/flights.js) lines 6–16 and [`src/worldFocus.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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):

```dotenv
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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`).

```javascript
// 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).

```javascript
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:

```javascript
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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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.

```javascript
// vite.config.js
export default defineConfig({
  plugins: [cesium()],
  server: {
    configureServer(server) {
      openSkyProxy().configureServer(server);
    }
  }
});

```

```javascript
// 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.