Airframe is the Crossplane-based service catalog behind Hangar. Eleven XRDs turn a git commit into a running service that is onboarded to CI/CD and has its secrets, without any cluster ever holding credentials for another cluster's API. I drew this on September 20, 2026 to see the wiring underneath: what each catalog object takes in and gives back, how the pieces compose, and what actually moves when someone asks for a service. It's a snapshot. The design doc is the living version, and it has moved on since.
xr-requests/ folder.Airframe holds exactly one live credential class: a GitHub personal access token for provider-upjet-github and, for secrets, an Infisical identity. It never authenticates to a second cluster's Kubernetes API. Every cross-cluster effect happens by committing a file into that cluster's own tenants repo and letting its own Argo CD pick it up.
argocd-platform is cluster-admin and installs the catalog itself. argocd-apps faces app owners, with one AppProject per app. The sets of repos they watch never overlap.
The engine's own Composition writes the very manifests Argo CD will later sync: identity.yaml, the release values.yaml, a SecretStore request. It closes the loop through git instead of calling any API directly.
An app is created from one of five Bootstrap-tier stacks. Every stack composes a TektonCICD child directly. That's a normal in-process composition, and it's safe because TektonCICD only ever calls the GitHub API. Everything that has to land on a different cluster's API, namely SecretStore, is requested the indirect way: a commit into that cluster's own xr-requests/, never a direct composition.
The graph shows shape; this is the full inventory. All eleven kinds share one API group and are namespaced. Only RolloutWatch publishes a real status.properties schema. Every other kind relies on custom conditions layered onto the standard Ready and Synced pair.
| Kind | Tier | Key spec fields (inputs) | Status outputs | Composes or requests |
|---|---|---|---|---|
| NodeJSApplication app stack | Bootstrap | devCluster *, description, nodeVersion (18|20|22), packageManager (npm|pnpm|yarn), port, visibility | DevClusterReady, CicdOnboarded, Ready | TektonCICD (direct) · SecretStore shared (xr-requests) |
| SpringBootApplication app stack | Bootstrap | devCluster *, description, javaVersion (17|21), buildTool (maven|gradle), groupId, port, visibility | DevClusterReady, CicdOnboarded, Ready | TektonCICD (direct) · SecretStore shared |
| GoApplication app stack | Bootstrap | devCluster *, description, goVersion (1.22 to 1.24), port, visibility (module path derived, not typed) | DevClusterReady, CicdOnboarded, Ready | TektonCICD (direct) · SecretStore shared |
| PythonApplication app stack | Bootstrap | devCluster *, description, pythonVersion (3.11 to 3.13), packageManager (pip|poetry|uv), port, visibility | DevClusterReady, CicdOnboarded, Ready | TektonCICD (direct) · SecretStore shared |
| InfraService app stack | Bootstrap | devCluster *, description, visibility. NodeJSApplication without the src repo and boilerplate, for shared infra with no app code | DevClusterReady, CicdOnboarded, Ready | TektonCICD type: infra · SecretStore shared |
| ApplicationEnvironment | Bootstrap | appName *, cluster * (live-gated: type=upper, crossplaneReady), env *, configMapGenerator | ClusterReady, WorkloadDeployed, Ready | SecretStore per env · a Usage blocks parent deletion |
| TektonCICD | Bootstrap-adjacent | appName *, type (app|infra), registerPipelinesAsCode, gitopsRepoUrl *, appRepoUrl *, devCluster * | DevClusterReady, CicdOnboarded | Composed only, never created by a developer directly |
| SecretStore | Attached (auto) | appRef.name *, cluster *, environmentSlug (default shared) | Ready | Requested only, via xr-requests, never created by a developer directly |
| Redis | Attached | environmentRef.name * (auto-stamped), size (small|medium|large), persistence | Ready | Rendered by the airframe-application chart from a components: block |
| SLO | Attached | environmentRef.name *, service *, objective * (0 to 100), indicator * (availability|latency) | Ready | Rendered from a slos: block; wraps Sloth |
| RolloutWatch | Attached | environmentRef.name *, appName *, cluster *, env *, notifications.slack | rolloutPhase, lastDiagnosisRevision, lastDiagnosisJob, lastDiagnosisTime | Rendered alongside any release with a rollout: block |
* marks a required field. All kinds: group catalog.hangar.io, scope Namespaced, apiextensions.crossplane.io/v2. This snapshot predates the rename: on September 20, 2026 the group was still catalog.idp.io.
The tier isn't a UI grouping. It's the mechanism. Bootstrap-tier XRs are created by a git commit and never touch a live cluster API, which is exactly why the engine can stay on one dev cluster however big the fleet gets. Attached-tier XRs compose real in-cluster resources directly, so they only mean something where Crossplane is actually running, and they have to be created per cluster.
| Tier | Members | Mechanism | Lifecycle |
|---|---|---|---|
| Bootstrap | NodeJS, SpringBoot, Go, Python and InfraService stacks, ApplicationEnvironment. TektonCICD is Bootstrap-adjacent: composed, not created by a developer. | A commit into xr-requests/, reconciled by provider-github. No live API call, no Kubernetes credential. | Creates a new addressable git location: a repo, or a <cluster>/<env>/values.yaml |
| Attached | SLO, Redis, RolloutWatch, and SecretStore (provisioned automatically, never chosen by a developer). Planned and unbuilt: OAuthServer, Database, Queue, mongodb, nginx. | A components: or slos: block inside that environment's own values.yaml, rendered into an XR in the app's namespace | Its own provisioning lifecycle, but only expressible inside an existing environment's file |
| Embedded | Config maps, secrets, HPA, PodDisruptionBudget, AnalysisTemplate, resource limits, volumes, networkPolicy | Plain fields on the same values.yaml, rendered directly, with no XR at all | One to one with the single workload (an Argo Rollout) the release owns |
One trace, start to finish: a developer asks for an app and an environment, the engine writes the git state that makes both real, and the target cluster ends up running a workload.
Every Composition in the catalog is a function-go-templating pipeline with inline source, plus a function-auto-ready step. The source stays inline because a second registered Function package once corrupted Crossplane's shared dependency-lock graph across the whole cluster. Below that shared shape, each XRD reaches a different real system.
The namespaced managed-resource family (repo.github.m.upbound.io), with a personal access token, because a GitHub App can't create repos under a personal account. It creates Repository and RepositoryFile resources: the src repo, its boilerplate, the empty gitops-<app> repo, identity.yaml and the release values.yaml. As of Sep 21, 2026 the boilerplate includes a real, build-only cicd.yaml, which closed the one hand-written step in an otherwise self-service flow.
Project, ProjectEnvironment, Identity and ProjectIdentity managed resources, wrapped through provider-kubernetes into a real ESO ClusterSecretStore. Crossplane v2 forces the wrap, since a namespaced XR can't compose a cluster-scoped resource directly. Kubernetes Auth on the cluster that hosts Infisical, Universal Auth everywhere else.
Redis composes a Release of a Bitnami-derived chart. It was the only component XRD with a real workload behind it. Every other planned component (Postgres, RabbitMQ, MongoDB, OAuthServer, nginx) was unbuilt.
SLO renders a Sloth PrometheusServiceLevel, which Sloth's own controller turns into a real PrometheusRule. RolloutWatch finds the live Rollout through a Crossplane extra-resources lookup and, when it goes Degraded, sends a diagnosis Job to that cluster's HolmesGPT service, which can open a fix PR back into the GitOps repo.
"No cluster ever holds credentials for another cluster's API."
That one rule explains every indirection on this page: why SecretStore goes through a GitOps commit instead of a direct composition, why the Bootstrap engine can stay on one dev cluster for good, and why each cluster's own argocd-apps is the only thing that ever touches that cluster's workloads.
Don't read these as built. Check the living design doc for where each one stands now.
Sources: the Hangar service catalog design and GitOps strategy docs, and the Airframe XRDs, as of September 20, 2026.