Connects flight planning systems, PPS first, keeps one canonical record per flight, and serves it to iPad, web and terminal clients: as an API, as events and as ARINC 633 flight folders.
When something happens to a flight, the hub tells the systems that asked for it. MK142 from Zurich to Naples is released for dispatch; this is what follows.
The event is stored with the change, in one database transaction:
{
"event_id": "7c9f3e2a-1b4d-4f8e-9a2c-3d5e6f7a8b9c",
"event": "flight.released",
"tenant_id": "b2f1c0de-…",
"flight_id": 90000022,
"dep": "LSZH", "dest": "LIRN",
"std": "2026-10-10T14:45:00Z",
"detected_at": "2026-10-10T11:03:12Z"
}
Two webhooks are registered; only one asked for releases:
✓ dispatch flight.released, flight.cancelled delivery queued – ops-dashboard flight.phase_changed not subscribed
Sent with a signature only you can check:
POST /hooks/pps X-PPS-Event: flight.released X-PPS-Event-Id: 7c9f3e2a-1b4d-4f8e-9a2c-3d5e6f7a8b9c X-PPS-Timestamp: 1791630192 X-PPS-Signature-V2: sha256=4e1d… HMAC of "timestamp.body"
Your system answers 503. The next attempts are already scheduled:
Up again before the third attempt: 200, delivered.
Back after a longer outage? Ask for everything since the last event you saw:
GET /v1/events?since=41 { "events": [ { "id": 42, "event_id": "7c9f3e2a-…", "type": "flight.released", … } ], "next_cursor": 42, "has_more": false }
Every change the hub sees and every acknowledgement a dispatcher makes goes into an evidence log. It cannot be edited quietly, not even by the database administrator.
A dispatcher acknowledges the fuel exception on MK142. The new link records the exception as it was, the plan revision, and the key that did it: issued to A. Meier.
Each link's hash covers its content and the hash of the link before it.
verify ok 45 links, chain intact
Someone edits link #44, the record of the plan revision, directly in the database.
verify hash #44: the content does not match its hash
So they recompute every hash after it. Now the chain is consistent again, but the anchor taken yesterday still holds the old head.
verify ok verify --anchors mismatch #44 anchored as 9c2e…, now b7a1…
Changes to airlines and API keys are recorded as well, in a separate admin audit log written in the same transaction as the change.
The ofp hub sits between the flight planning systems and the people who dispatch and fly. Each planning system connects through its own adapter, and all of them feed one canonical record per flight. Clients, whether iPad and web apps or the terminal client ofp, talk only to the hub, never to a planning system directly.
The loop runs both ways. Out to the cockpit go electronic flight folder packages in ARINC 633 Supplement 5, generated from the canonical record. Back from the cockpit comes what the crew records on the client: the loadsheet, block times, fuel and delays. It lands in the same record and moves the flight through its phases.
PPS is the first system connected. The hub turns its SOAP web service into a plain REST interface: flights, OFPs, NOTAMs and weather are one HTTP call each, with JSON in return. The canonical model is built for more than one planning system; further adapters are planned.
For PPS, the canonical record is kept fresh from its change feed, so a quiet minute costs a few hundred bytes instead of a full flight list. A state machine tracks each flight through its operational phases, and webhooks tell other systems when something changes: signed, retried, replayable, with a catch-up call for missed events. Analytics answer questions PPS itself does not: planned against actual fuel by route, and delays by IATA delay code.
For dispatchers, an exception board lists the flights that need a second look: recalculated or withdrawn OFPs, fuel plans that moved by 5 percent or more, NOTAMs that close a runway. A dispatcher can acknowledge an exception, and an append-only evidence log records what changed and who acknowledged what, on which information. Each entry is chained to the one before it by hash, so an edited or deleted row is detected, and a daily anchor of the chain head can be kept somewhere the database administrator cannot write.
The terminal client ofp shows the flight list and the exception board, and opens a flight's route map in the browser. A demo mode runs the whole system against a mock PPS, with simulated crew reports, so it can be shown without touching any real data.
Next: a navigation database as a further source, kept per AIRAC cycle. With it the hub can check a route against the current cycle, filter NOTAMs by the airspace a route actually crosses, and give the clients procedures and airspaces for their maps.
About 650 automated tests cover the hub. In its code and its downloads it is still called pps-api.
The service has several API layers on top of a PostgreSQL database. All routes are versioned under /v1/.
| Layer | Path prefix | Purpose |
|---|---|---|
| PPS Proxy | /v1/pps/* | Translates REST calls into live SOAP requests |
| Canonical Hub | /v1/flights/* | Persistent flight records with full lifecycle tracking |
| Dispatch | /v1/dispatch/* | Exception board and acknowledgements |
| Evidence | /v1/evidence/* | The append-only change log: read, verify, check anchors |
| Webhooks | /v1/webhooks/*, /v1/events | Signed event delivery, replay and catch-up |
| Analytics | /v1/analytics/* | Aggregated fuel, delay, and route statistics |
A background sync polls PPS's change feed every 60 seconds and runs a full pass every 10 minutes, which also notices flights that have vanished. It keeps the local mirror fresh and fires webhook notifications on phase and status transitions. Only one instance syncs at a time, so extra replicas do not multiply the load on PPS.
Every canonical flight moves through a defined lifecycle driven by submitted operational data:
scheduled → pre_flight → airborne → landed → completed
A flight that disappears from PPS ends as cancelled.
scheduled — flight exists in PPS, no crew data yetpre_flight — W&B, PAX count, and fuel uplift submittedairborne — AOBT (off-block time) recordedlanded — ALDT (landing time) recordedcompleted — AIBT (in-block time) submitted, or 24 h automatic timeoutNext to it runs a second, independent state: the PPS status of the flight (planned, released, modified, recalculated), derived from PPS's own signals.
PPS Proxy
POST /v1/auth/login
GET /v1/pps/flights/by-std?from=&to= # list by departure window
GET /v1/pps/flights/{id} # full OFP — route, fuel, weights, crew
GET /v1/pps/flights/{id}/atc # ICAO 4444 ATC flight plan
GET /v1/pps/flights/{id}/eff # EFF briefing package download
POST /v1/pps/flights/{id}/refresh # re-fetch OFP from dispatcher
POST /v1/pps/flights/batch # up to 20 OFPs concurrently
GET /v1/pps/airports/notams # active NOTAMs by ICAO code
GET /v1/pps/flights/{id}/weather # TAF and METAR along the route
Canonical Hub
GET /v1/flights # paginated, filterable by date/route/phase
GET /v1/flights/{uuid} # full record with ARINC 633-5 pre-flight data
PUT /v1/flights/{uuid}/pre-flight # submit W&B, PAX, fuel uplift
PUT /v1/flights/{uuid}/post-flight # submit block times, fuel actuals, delays
GET /v1/flights/{uuid}/phase # lightweight phase poll
GET /v1/flights/{uuid}/route-geojson # the route as GeoJSON for map clients
POST /v1/flights/{uuid}/eff/generate # ARINC 633-5 EFUSUB document from canonical data
Dispatch and evidence
GET /v1/dispatch/exceptions # the exception board
PUT /v1/dispatch/exceptions/{id}/ack # acknowledge one exception
GET /v1/evidence # read the change log
GET /v1/evidence/verify # check the hash chain
Webhooks and analytics
POST /v1/webhooks # register a receiver
POST /v1/webhooks/{id}/replay # replay events after a cursor
GET /v1/analytics/fuel # burn aggregates by route and aircraft type
GET /v1/analytics/delays # IATA delay codes + on-time performance
GET /v1/analytics/routes # per-route stats: count, fuel, delays, distance
Each row is written in the same database transaction as the change it describes, so neither exists without the other. It records exceptions appearing, changing and coming back after an acknowledgement, flight status changes, and OFP revisions (with a SHA-256 of the content). A row names the authenticated API key and its label next to the free-text name the client claims, which is not proof of identity; one key per person makes an acknowledgement attributable to that person. Rows have no foreign keys, so the evidence outlives the flight or tenant it describes. A hash chain shows tampering but cannot stop an administrator from rewriting a whole chain, which is why the chain head is also exported daily to a file that can be kept elsewhere and checked later.
| Component | Technology |
|---|---|
| Web server | Axum 0.8 + Tokio async runtime |
| Database | PostgreSQL 16 via SQLx (parameterised queries, no ORM) |
| SOAP client | reqwest + quick-xml |
| Auth | Per-tenant hashed API keys with scopes and labels |
| API docs | utoipa + Swagger UI at /swagger-ui/ |
| Terminal UI | ratatui + Clap subcommands |
| Web client | Vite + React + TypeScript, deck.gl route visualisation |
| Packaging | Docker Compose; one small ARM server is enough |
pps-cli crate): flights, the exception board, handover notes, the evidence log and analytics, and the route map in the browser; a free download for macOS and Linux