Docs · Airframe
Skyport — the demo system
Skyport is one small, believable airport system split across five services. Its job is to exercise every Airframe stack and every component in one place, so each feature has a real caller and a visible effect instead of a toy curl.
The code lives in examples/skyport/
in the airframe repo. Why here and not hangar: the demos are consumers of
Airframe’s XRDs and versioned alongside them, while hangar is the docs umbrella.
Each service directory is what you copy into the source repo Airframe scaffolds for
you (see the quickstart, section 03).
Status.
boarding-api(Phase 0) andflight-api(Phase 1) exist. Everything else is a plan (six AI-workload phases were added 2026-09-26, see below). The components the later phases depend on — RabbitMQ, MongoDB, OAuth server — are unbuilt in Airframe today; each phase below lists what has to be built first. Postgres is built: a dedicated CloudNativePG cluster per app environment.
The five services
| Service | Airframe stack | Components | What it does |
|---|---|---|---|
| boarding-api | NodeJSApplication |
Redis · RabbitMQ (consumer) | The passenger-facing gate board: flight/gate lookups, boarding-pass scans, and the canary visualizer. Caches lookups in Redis. |
| flight-api | SpringBootApplication |
Postgres · RabbitMQ (producer) · OAuth (resource server) | System of record for flights and gates. A scheduled simulator delays flights and changes gates, recording an event each time (published to the broker from Phase 2). |
| baggage-api | PythonApplication |
RabbitMQ (consumer) · MongoDB | Tracks each bag’s journey as a document. Consumes flight events to re-route bags when a gate changes. |
| skyport-auth | InfraService |
oauth-server |
The OIDC provider. Issues the tokens flight-api and baggage-api validate. |
| skyport-broker | InfraService |
rabbitmq |
The shared message broker flight-api, baggage-api and boarding-api all attach to. |
GoApplication is deliberately left out; the other four Bootstrap stacks
(NodeJS, SpringBoot, Python, InfraService) are all used, and InfraService
twice — it’s the stack for things several services share.
An InfraService is an empty GitOps deploy repo with no source, so the two shared
pieces are components hosted there rather than code.
How they work together
browser
│
▼
┌───────────────┐ token ┌───────────────┐
│ boarding-api │──────────▶ │ skyport-auth │
│ (NodeJS) │ │ (OIDC) │
│ Redis cache │ └───────┬───────┘
└──┬─────────▲──┘ │ JWKS
GET │ │ gate-changed ▼
/flights │ │ (evict cache) ┌───────────────┐ flight.* ┌───────────────┐
▼ │ │ flight-api │──────────────▶│skyport-broker │
└────────────────│ (Spring) │ │ (RabbitMQ) │
│ Postgres │ └──────┬────────┘
└───────────────┘ │ flight.*
▼
┌───────────────┐
│ baggage-api │
│ (Python) │
│ MongoDB │
└───────────────┘
A day in the life:
- Look up a flight. The gate board asks
boarding-api. On a Redis miss it callsflight-api(with a bearer token fromskyport-auth), which reads Postgres. The answer is cached for three minutes. - Scan a boarding pass.
boarding-apiincrements a per-flight counter in Redis. With two replicas this is only correct because Redis is shared — the gate board shows which cache mode it’s in, and in-memory mode visibly miscounts. - Something changes.
flight-api’s simulator moves a flight to another gate and publishesflight.gate-changed. - Two consumers react.
boarding-apievicts the cached entry, so the next lookup shows the new gate at once instead of after the TTL.baggage-apire-routes that flight’s bags and updates their documents in MongoDB.
The events are the point of the design: cache invalidation and bag re-routing are two unrelated consumers of one fact, which is exactly what a queue is for.
Which component does what
| Component | Used by | Demonstrates |
|---|---|---|
| Redis | boarding-api | Cache-aside with TTL, shared counters across replicas |
| Postgres | flight-api | A relational system of record, schema migration on deploy, credentials read straight from the component’s Secret |
| RabbitMQ | flight-api → boarding-api, baggage-api | Fan-out events; a shared broker via InfraService |
| MongoDB | baggage-api | Document storage that doesn’t fit rows |
| OAuth server | flight-api, baggage-api, boarding-api | Service-to-service tokens; one issuer for the platform |
| SLO / RolloutWatch / SecretStore | all five | The components that already exist, on every service |
The canary test bench
boarding-api is built to make progressive delivery visible. The gate board polls
/api/whoami twice a second and draws a bar of which version answered:
- The header colour is derived from the answering version, so a v1 → v2 flip is obvious without reading anything.
- v2.0.0 adds a “boarding group” line to the lookup result, so a canary also changes behaviour, not just a number.
- Each poll opens a fresh connection (
Connection: close), so even throughkubectl port-forward— which pins a connection to one pod — the tally reflects the pod mix instead of showing 100% of whichever pod you landed on.
The quickstart walks a real canary with it. Blue/green works the same way: the bar flips from 100% v1 to 100% v2 at promotion instead of ramping.
AI workloads: one per workload shape
Planned 2026-09-26. Skyport also runs six AI agents, one for each shape Autopilot supports, so every part of it has a real caller and a test that can fail. They read Skyport’s APIs, draft, store artifacts, ask a human and spawn narrower runs. None of them can apply a change.
| Shape | Agent | What it does |
|---|---|---|
| Task | flight-briefer |
An ops brief for one flight from the three APIs. |
| Session | gate-copilot |
A chat in Tower for a gate agent; drafts announcements, never sends. |
| Service | passenger-assistant |
Always-on flight-status chat for passengers. An ordinary application. |
| Scheduled | delay-digest |
A daily delays and gate-changes report. |
| Event | disruption-responder |
On flight.*.delayed it drafts rebooking notices; a trigger bridge (an Airframe app on the RabbitMQ attach) starts one run per unique message. |
| Team | irregular-ops-team |
A planner with researcher, drafter and checker workers, each narrower than its parent. |
Definitions and their tests exist (clearance/agents/skyport/, six Preflight cases); the runtime that
would run them (Clearance, the AgentRun claim, a model proxy) is not built yet. The design, the
safety demonstrations (prompt injection, redelivered events, an over-broad spawn, a sandbox the dev
cluster cannot provide) and the build order are in
hangar/docs/autopilot/skyport-ai-workloads.md.
Diagram: hangar/docs/autopilot/diagrams/plan/07-skyport-ai-workloads.html.
Phases
Each phase ends with something you can run. Nothing later than Phase 1 is built.
| Phase | Adds | Must exist first | Status |
|---|---|---|---|
| 0 | boarding-api (NodeJS) + Redis + canary UI — quickstart |
Redis component, provider-helm on the target cluster |
Deployed on the dev cluster with Redis. The canary and flight environment are the parts not yet walked. |
| 1 | flight-api (Spring) + Postgres; boarding-api calls it — quickstart part 2 |
postgresql component (built) |
Code written and tested against a real Postgres, and boarding-api verified against it. Not yet deployed through Airframe. |
| 2 | skyport-broker (RabbitMQ), flight events, baggage-api (Python), cache eviction |
rabbitmq component (built: one shared broker per cluster/env, attach per app) |
Broker, flight-api (publisher) and boarding-api (consumer/cache eviction) built and walked on the dev cluster: quickstart part 3. baggage-api (Python consumer of flights.events, re-routes bags between carousels on a gate change) built and verified on dev on 2026-09-26: a gate change in flight-api moved AC123’s bags from carousel 2 to 3 within seconds. Its staging environment is not created yet, and nothing is on the prod cluster for it. State is in memory, so it runs one replica until phase 3. |
| 3 | MongoDB for baggage-api |
mongodb component |
Planned |
| 4 | skyport-auth and enforced JWTs |
oauth-server component; Keycloak-vs-alternative decision |
Planned |
| 5 | (optional) an nginx edge as a third InfraService |
nginx component; its scope is still undecided |
Planned |
| 6 | First agent: flight-briefer (task) |
Autopilot core: Clearance, AgentRun, a model proxy |
Planned; not started |
| 7 | gate-copilot (session) |
session channel and Tower’s Agent tab | Planned |
| 8 | passenger-assistant (service) |
the chart’s agent: block; a Redis attach |
Planned |
| 9 | delay-digest (scheduled) |
trigger runner, artifact store | Planned |
| 10 | disruption-responder (event) |
trigger bridge on the RabbitMQ attach | Planned |
| 11 | irregular-ops-team (team) |
run.spawn, approvals |
Planned |
Phases 6 to 11 are numbered in the order they will be built, and slot in after phases 3 and 4 because
the agents read baggage-api. Phase 5 (nginx) moves after them. The unified plan is
hangar/docs/autopilot/roadmap.md.
RabbitMQ’s questions are settled (shared broker, provider-rabbitmq not used). The open
design questions behind Phases 3–5 — shared vs dedicated tenancy for Mongo, what nginx
covers — are in hangar/docs/service-catalog-design.md. This plan doesn’t settle them.
Two architectures
The dev cluster (ground) is arm64 and the prod cluster (flight) is amd64. Everything in Skyport has to run on both:
- Application images. The build stage builds
linux/arm64andlinux/amd64by default (build.platformsincicd.yaml); leave it alone. The amd64 leg runs under QEMU on the arm64 build node, so it is slow, and a compiler crashing under emulation is a known failure mode. Where a build stage is architecture-neutral (Java bytecode, pure-JS dependencies) do not rely onFROM --platform=$BUILDPLATFORM: this platform’s builder (kaniko) ignores it and the stage still runs emulated (tested; the build stage reportedx86_64andBUILDPLATFORMwas empty). Compile inbuild.scriptinstead, natively in the build agent, and keep the Containerfile a thin packaging step.flight-apidoes this; the scaffold’s Java Containerfile, which compiles inside the image build, took about 11 minutes for the emulated leg (about 17 for the whole image).boarding-apihas no native dependencies.baggage-apiis pure Python (pika), so it builds natively on either architecture.flight-apiandbaggage-apimust avoid ones without both wheels or classifiers. - Python images and the scan gate (baggage-api).
python:3.13-slim(Debian 13) failed the Trivy gate with 44 HIGH util-linux/acl CVEs that have no fix yet.python:3.13-alpineclears the OS findings, but the vendored copies ofmsgpackandsetuptoolsinside pip and setuptools then fail it (GHSA-6v7p-g79w-8964, CVE-2025-47273); upgrading does not remove them, so the Containerfile runspip uninstall -y setuptools pipafter installing dependencies. The scaffold’sContainerfileshould do the same (a scorecard item). - In-memory state and replicas (baggage-api). Every replica declares the same queue and they compete for its messages, so with two replicas each holds half of the event history. Run one replica until the state moves to MongoDB (phase 3).
- First deploy. Configure an app before it has an image with
rollout: nullon chart versions before v0.3.91; from v0.3.91 the chart renders no workload until an image exists, so ordinary rollout config is safe. - Component charts. Every image a wrapped upstream chart pulls has to be a
multi-arch index. Checked for Redis (Bitnami
redis:latest: amd64 and arm64); check each new component before it ships — this is the easiest thing to get wrong. - Crossplane Functions and providers. Any function or provider image this
catalog publishes must be multi-arch, because Crossplane runs on both clusters.
function-rollout-watcheralready is. The platform toolbox image (platform-cicd-toolbox) is arm64-only, which is fine only because pipelines run on the dev cluster; a pipeline on the prod cluster would fail withexec format error.