The catalog
The catalog is where Airframe keeps its intent: one git repository of small YAML files that say which products exist, who owns them and what each one needs. Everything else the platform builds, from cloud projects to dashboards, is derived from these files. Changing the organization therefore means changing a file, through a merge request that the right people approve.
Why intent lives in files
Section titled “Why intent lives in files”Most organizations keep this information in several places at once: a spreadsheet of services, a portal form, the memberships of a dozen tools. Those copies drift apart, and nobody can say which one is right.
Airframe keeps one copy. Because the catalog is a git repository, every change to it is a merge request: reviewed before it happens, recorded with who made it and why, and easy to undo. There is no separate inventory to keep in sync, because the catalog is the inventory.
How the organization is divided
Section titled “How the organization is divided”The catalog describes the organization as a tree with four levels:
- a journey is a broad area of the business; the demo organization has three:
shop,fulfilandsettle; - a domain sits in a journey, such as
storeinshop; - a context sits in a domain, such as
orderinginstore: a group of closely related products and the teams that build them; - a product sits in a context, such as
ordersinordering.
Each level is a file named by its full path: journeys/shop.yaml, domains/shop.store.yaml, contexts/shop.store.ordering.yaml and products/shop.store.ordering.orders.yaml. A file names its level’s leads, the people who approve some kinds of change below it. A name only has to be unique among its siblings, because the path is the identity, and the catalog’s checks refuse a file whose parent does not exist.
Two more kinds of file describe people. A team file, such as teams/store-core.yaml, says which context the team lives in, who leads it, who its members are and what role each one has (developer, team lead, on call), and how its on-call rotation works. A person file holds a name, an email address in the organization’s domain and a status: active, pending (no account yet) or departed.
What a product file holds
Section titled “What a product file holds”Here are the main fields of the orders product’s file, as they are in the demo catalog:
product: ordersjourney: shopdomain: storecontext: orderingteam: store-corecriticality: 2data_classification: internalrto: 1hrpo: 15mmonthly_budget: 5stack: nodeslo: { availability: 99.5, latency_p95_ms: 400 }sandbox: truelifecycle: { status: retiring, retire_after: "2099-12-31" }In plain words:
teamis the team that owns the product; its members can approve changes to it.criticalitysays how much the business suffers when the product fails, from 1 (it stops the order path) to 3 (internal tooling). It drives, for example, the database tier and the alert thresholds.data_classificationsays how sensitive its data is, frompublictorestricted; a restricted product gets a private repository in a restricted part of GitLab.rtoandrpoare its recovery objectives: how quickly it must be back after a failure, and how much recent data it may lose. They set its database’s backups.monthly_budgetis in US dollars, production and non-production together. It is a limit, not a note: a merge request whose priced plans exceed it cannot merge.stackchooses the application the golden path generates: Node, Python or Java.slois its reliability target, which its alert rules use.sandboxasks for a stable non-production copy that other products can test against.lifecyclesays whether the product is active, retiring or removed.
What is derived from it
Section titled “What is derived from it”When a product file is merged, the reconcilers create, among other things:
- the repository
projectairframe/shop/store/ordering/orders, with its first commit generated from the golden path’s template; - two Google Cloud projects,
lk-store-orders-<rnd>andlk-store-orders-np-<rnd>, with a plan identity and an apply identity in each; - the Google group of the product’s users, which is the audience its hostname admits behind single sign-on;
- its dashboards and alert rules in Grafana;
- once its projects are billed, a Google Cloud budget on each, 70 percent of the monthly budget for production and 30 for non-production.
Billing itself is not in the catalog: a product’s projects are billed by adding it to a list in the landing-zone repository, a separate merge request. The reconcilers page explains the chain that does this work.
Why two projects per product
Section titled “Why two projects per product”Every product gets two Google Cloud projects: one for production, and one for everything that is not production, such as its previews and its sandbox. This is the design, not a step on the way to sharing: the project factory of the product’s domain creates both from the catalog entry, for every product.
The reason is the permission boundary. A product’s plan and apply identities hold their roles on the product’s own projects, so the identity that applies a product’s Terraform can change that product and nothing else. A mistake in that code, or a stolen pipeline token, can damage one product at most. A project shared by several products cannot offer that: an identity able to change one of them could change all of them. Quotas and audit logs are kept per project, and the factory gives each billed project its own budget, so each product has its own. The second project keeps previews, their identities and their test data outside production’s boundary.
What more projects cost, and what they do not:
- Google Cloud charges nothing for a project. What does multiply with products is what each product has to itself, such as its own Cloud SQL instance, and that comes from keeping products apart, not from the number of projects.
- Nobody creates or administers a product’s project by hand. The factory creates each one with its identities, permissions and labels, and gives it a budget once it is billed; the reconcilers attach the billed ones to the shared network. The platform team keeps catalog entries, not projects.
- Most Google Cloud quotas apply per project, such as the number of Cloud SQL instances and service accounts and the rate of API requests. Separate projects give each product its own limits, so one team’s busy pipeline cannot use up another team’s API quota.
Two things do grow with the number of projects, and are worth checking before adopting Airframe:
- Projects per billing account. A billing account can link only so many projects to billing, and each billed product takes two. Airframe’s own could link five at first. Raising the limit is a request to Google, as the prerequisites explain.
- Tools priced per project. If your organization pays for a tool by the number of Google Cloud projects, as some security and monitoring products charge, count two for each product.
The lever is what counts as a product. The unit is the boundary of a workload, not each application. Several small tools of one team, with the same criticality and data classification, can be one product with several services: one pair of projects and one set of pipeline identities. The design adds two refinements, and neither is built. A product with one part that differs in criticality or data classification could declare that part as a component, and only the component would get a project pair of its own: the catalog accepts the components field, and no project factory reads it yet (designed). And products could share compute, in namespaces on one GKE cluster per environment, while their data, identities, secrets and state stay in their own projects: no cluster exists yet (designed). The pages on catalog fields and bringing an existing system say more.
Access is not granted project by project. A team’s read access to its products’ projects and their logs comes from the catalog: the factory gives those roles on every project of the team’s products to the team’s Google group, and the groups root fills the group from the team’s file. The administrative grant a person can request during an incident, level 2, is defined once, on the demo organization’s folder, so it covers every product project, new ones included. Identities and access and the grants explain both.
Who approves what
Section titled “Who approves what”The catalog also says who must approve each change, in one file: ownership/protected-paths.yaml, the ownership model. For changes to the catalog itself, it says:
- a product file is approved by the product’s team, or the leads of its context or its domain;
- raising a product’s budget, or letting it use a new Google Cloud service, needs a domain lead;
- a change of lifecycle status needs two approvers: one from the product’s team and one from the domain’s leads or the platform team, and the merge request must carry the
lifecyclelabel; - a team file is approved by its team leads or its context’s leads;
- a person’s own profile needs that person: their approval, or a merge request they opened whose every commit is theirs; the rest of their file needs the leads of their teams;
- the structure, meaning the journeys, domains and contexts, the ownership model, the schema and the approved services, needs two members of the platform team.
The author of a change and the authors of its commits never count as approvers. A person who changes their own profile still needs one other named person’s approval, which the group-wide approval policy asks of every merge. In the demo, the fictional leads have no GitLab account, so the platform team stands in for them.
The same file also covers each product’s repository: for example, its infra/ directory needs the team’s approval, and its pipeline file needs one approval, from the team or the platform team. The gate reads it directly on every merge request, always from the catalog’s protected main branch, never from the change it is judging. Each repository also has a CODEOWNERS file. A product’s is written once, when the repository is made, and names its context’s group, and the platform repositories’ files are not checked against the model. The catalog’s own file is rendered from the model by the gate’s own renderer, and control-plane’s catalog:codeowners job compares the two after every catalog merge, where a difference warns, and every day, where a difference fails and an issue stays open until a merge request in the catalog regenerates the file. Either way the gate, not CODEOWNERS, applies the model.
Lifecycle: active, retiring, removed
Section titled “Lifecycle: active, retiring, removed”A product is never simply deleted. It moves through three states, each a change to its file:
- active: the product runs.
- retiring: its services leave the shared edge and scale to zero, and every project, database and bucket is kept.
- removed: allowed only after the
retire_afterdate in the entry. The product’s projects and its repository are deleted.
A removed product leaves a tombstone: a file in retired-names/, such as retired-names/shop.store.returns.yaml for a product removed on 2026-09-14. The catalog’s checks refuse any new product with that name anywhere in the domain, and refuse any change to a tombstone, so a name is never reused.
Where it lives
In the catalog repository:
- products/: one file per product
- teams/: one file per team
- ownership/protected-paths.yaml: the ownership model
- schema/: the JSON Schemas every file is checked against
- retired-names/: the tombstones
- scripts/lint.mjs: the catalog's consistency checks
In the landing-zone repository:
