Airframe · Architecture · Service catalog

Airframe, the service catalog under Hangar

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.

Architecture September 20, 2026Snapshot September 20, 2026Living doc Service catalog design
Snapshot · Sep 20, 2026

Everything routes through git.

The shape of a request

Everything routes through gitA developer commit lands in GitHub, Argo CD syncs it to the Crossplane-based Airframe engine, which writes back to GitHub and provisions Infisical secrets. GitHub onboarding triggers Tekton CI/CD, and Argo CD deploys the resulting release to the target cluster.DeveloperTower · BackstageGitHubtenants · gitops · srcArgo CDplatform + appsTarget clusterRollout · ServiceTekton CI/CDGlidepath · PaCAirframe engineXRDs, CompositionsInfisicalprovider · ESOCOMMIT XRSYNCDEPLOYAPPLYWRITESECRETSONBOARDIMAGE PRINJECTNo call ever reaches a second cluster's API. Cross-cluster effects are commits.LEGENDenginecontrol planegitexternalhumanGitHub calldeployasync
Figure 1Every cluster runs two Argo CD instances: argocd-platform, cluster-admin, which installs the catalog itself, and argocd-apps, app-owner facing, with one AppProject per app. Their watched repos never overlap.
The trust boundary

Git-only, on purpose

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.

Every cluster

Two Argo CD instances

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 loop

Why the write loops back

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.

The catalog: eleven XRDs, one composition graph

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.

Eleven XRDs, one composition graphAn app stack XRD composes a TektonCICD child directly and requests a SecretStore indirectly. An ApplicationEnvironment belongs to an app, also requests its own SecretStore, and is where Redis, SLO and RolloutWatch attach.DevClusterReady · CicdOnboardedApp stack (5 XRDs)NodeJS · Spring · Go · Python · InfraClusterReady · WorkloadDeployedApplicationEnvironmentappName · cluster · envTektonCICDcomposed only; GitHub APICicdOnboardedSecretStoreper env, or sharedReadyRedissize, persistenceSLOobjective, indicatorRolloutWatchrolloutPhaseATTACHES · 1:NHAS ENVS1NCOMPOSES · 1:1COMMITSCOMMITSLEGENDaggregate rootXRDdirect compositionindirect: xr-requests commit
Figure 2The small mono text above or below a box is its published status. Anything that has to land on a different cluster's API is requested by a commit, never composed directly.

Every input, every output

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.

KindTierKey spec fields (inputs)Status outputsComposes or requests
NodeJSApplication
app stack
BootstrapdevCluster *, description, nodeVersion (18|20|22), packageManager (npm|pnpm|yarn), port, visibilityDevClusterReady, CicdOnboarded, ReadyTektonCICD (direct) · SecretStore shared (xr-requests)
SpringBootApplication
app stack
BootstrapdevCluster *, description, javaVersion (17|21), buildTool (maven|gradle), groupId, port, visibilityDevClusterReady, CicdOnboarded, ReadyTektonCICD (direct) · SecretStore shared
GoApplication
app stack
BootstrapdevCluster *, description, goVersion (1.22 to 1.24), port, visibility (module path derived, not typed)DevClusterReady, CicdOnboarded, ReadyTektonCICD (direct) · SecretStore shared
PythonApplication
app stack
BootstrapdevCluster *, description, pythonVersion (3.11 to 3.13), packageManager (pip|poetry|uv), port, visibilityDevClusterReady, CicdOnboarded, ReadyTektonCICD (direct) · SecretStore shared
InfraService
app stack
BootstrapdevCluster *, description, visibility. NodeJSApplication without the src repo and boilerplate, for shared infra with no app codeDevClusterReady, CicdOnboarded, ReadyTektonCICD type: infra · SecretStore shared
ApplicationEnvironmentBootstrapappName *, cluster * (live-gated: type=upper, crossplaneReady), env *, configMapGeneratorClusterReady, WorkloadDeployed, ReadySecretStore per env · a Usage blocks parent deletion
TektonCICDBootstrap-adjacentappName *, type (app|infra), registerPipelinesAsCode, gitopsRepoUrl *, appRepoUrl *, devCluster *DevClusterReady, CicdOnboardedComposed only, never created by a developer directly
SecretStoreAttached (auto)appRef.name *, cluster *, environmentSlug (default shared)ReadyRequested only, via xr-requests, never created by a developer directly
RedisAttachedenvironmentRef.name * (auto-stamped), size (small|medium|large), persistenceReadyRendered by the airframe-application chart from a components: block
SLOAttachedenvironmentRef.name *, service *, objective * (0 to 100), indicator * (availability|latency)ReadyRendered from a slos: block; wraps Sloth
RolloutWatchAttachedenvironmentRef.name *, appName *, cluster *, env *, notifications.slackrolloutPhase, lastDiagnosisRevision, lastDiagnosisJob, lastDiagnosisTimeRendered 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.

Three tiers, three lifecycles

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.

TierMembersMechanismLifecycle
BootstrapNodeJS, 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
AttachedSLO, 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 namespaceIts own provisioning lifecycle, but only expressible inside an existing environment's file
EmbeddedConfig maps, secrets, HPA, PodDisruptionBudget, AnalysisTemplate, resource limits, volumes, networkPolicyPlain fields on the same values.yaml, rendered directly, with no XR at allOne to one with the single workload (an Argo Rollout) the release owns

From request to running service

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.

From request to running serviceA developer commits an XR to GitHub; Argo CD applies it to the Airframe engine, which composes TektonCICD, writes repos and files back to GitHub and marks itself ready. Argo CD syncs the release to the target cluster, and once a later image PR merges, the workload is marked deployed.DeveloperGitHubAirframeArgo CDTarget clusterCOMMIT XR-REQUESTS/*SYNC DETECTS XRAPPLY XRCOMPOSE TEKTONCICD CHILDWRITE REPOS AND FILESDevClusterReady · CicdOnboardedSYNC RELEASEAPPLY ROLLOUT · SERVICEIMAGE PR MERGED, LATERRE-SYNC IMAGEWorkloadDeployedLEGENDGitHub write or commitsync or applydeployasync, later
Figure 3Four status conditions gate the handoff, DevClusterReady, CicdOnboarded, ClusterReady and WorkloadDeployed, and each turns True only once its real precondition is met, not on a timer.

What each Composition actually calls

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.

GitHub

provider-upjet-github

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.

Secrets

provider-infisical and ESO

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.

Compute

provider-helm

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.

Observability

Sloth and HolmesGPT

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.

Not yet live on September 20, 2026

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.