Aether: OpenSky Integration and the Zero-Credit Architecture
One week after launching Aether, the flight sharing service has gone through a significant evolution. The original design had every Flutter client talking to OpenSky Network directly. That worked for one user. It would not work for a hundred.
The credit problem
OpenSky Network provides free aircraft tracking data via a REST API. Authenticated accounts get 4,000 credits per day. A single /states/all query costs 4 credits.
In the first version, each Flutter client polled OpenSky every 30 seconds for position updates on tracked flights. That is 5 credits per query, 10 credits per minute, per user. Ten users watching one flight would burn 100 credits per minute. The daily budget would be gone in 40 minutes.
Flight search was worse. Auto-search-on-every-keystroke meant a user typing “UA456” could fire four queries before finishing the input. Multiply by active users and the budget evaporates.
Server-side position cache
The fix is straightforward: the Aether API on Railway now polls OpenSky in the background, once per active flight per 60 seconds. Results are cached in memory. Flutter clients read from the cache — zero OpenSky credits consumed per client request regardless of user count.
GET /api/position/:callsign/states/allThe cost model inverts completely. Instead of cost scaling linearly with users, cost scales only with active flights. One flight costs 4 credits per minute whether one person or a thousand people are watching it.
Server-side search cache
Search followed the same pattern. The Aether API now polls OpenSky /states/all (the full global state vector dump — around 6,000 aircraft) every 15 minutes and caches everything in memory. The Flutter app searches against this cache via GET /api/flights/search. Client-side credit cost: zero.
The search endpoint returns callsign, ICAO24, origin country, position, altitude, speed, and on-ground status. Results are sorted with exact prefix matches first, then substring matches. The front-end adds explicit submit (keyboard Search button or icon tap) instead of our earlier auto-search-on-keystroke. A generation counter prevents the classic race condition where slow responses for partial queries overwrite newer results.
Fixed server cost: 384 credits per day (4 credits per poll, 4 polls per hour, 24 hours). Under 10% of the daily budget.
Railway cannot reach OpenSky
A discovery during deployment: Railway’s network cannot reliably reach opensky-network.org at IP 194.209.200.34. Connections time out or reset intermittently. The workaround is a Cloudflare Worker that acts as a transparent proxy. The Aether API calls the Worker, which forwards the request to OpenSky with Basic auth credentials. Added authentication via a shared X-Proxy-Key header. Only GET requests to /api/states/ and /api/flights/ are proxied.
This is the kind of infrastructure problem that burns hours to diagnose and thirty lines of code to fix.
Route enrichment and its limits
When a user searches for flights, we enrich results with departure and arrival airports from OpenSky’s /flights/aircraft?icao24= endpoint. This works well for landed flights where OpenSky has correlated the trajectory with known airports. For in-flight aircraft, OpenSky usually knows the departure airport (estimated from takeoff position proximity) but cannot determine the destination.
The search results now show this honestly: “From KDFW - En route” when departure is known but arrival is not. This is an API limitation, not a bug. Heading-based destination estimation was considered and rejected — a plane heading west from Dubai could be going to hundreds of airports.
The new scheduling UX
The schedule flight screen received a full overhaul to take advantage of the server-side data:
Airport picker. A full-screen picker with 900+ airports including military airfields. Search by name, IATA or ICAO code, and country. When GPS is available, airports are sorted by distance from the user’s current location.
Live flight data overlay. When the user selects a flight from search results, the schedule form populates automatically. As they scroll down to fill in remaining details, a frosted-glass sticky header slides in from the top showing real-time altitude, speed, heading, coordinates, origin country, and route status. This follows the same slide/fade/blur animation pattern as the active authors header in the Signals feed.
Conflict detection. The app warns if the scheduled flight overlaps with an existing active flight’s time window.
Skeleton loading. The initial flight list shows shimmer placeholders while data loads, replacing the blank screen flash.
Credit budget today
Current daily burn for a deployment with 10 concurrent active flights:
| Source | Credits/day |
|---|---|
| Position polling (10 flights x 4 credits x 1/min x 24h) | 5,760 |
| Search cache (4 credits x 4/hour x 24h) | 384 |
| Total server-side | 6,144 |
| Client-side | 0 |
The position polling dominates, but it scales with active flights, not users. A more typical 2-3 active flights stays well within the 4,000 daily budget. For heavier usage, the poll interval is configurable via OPENSKY_POLL_INTERVAL_S.
Before this architecture: 10 users watching 10 flights burned approximately 14,400 credits per day. After: the same scenario costs 6,144 credits regardless of whether 10 or 10,000 users are watching.