InsightsPlatform Engineering
What to put in a Backstage software catalog, and how to keep it true
Which entities and fields belong in catalog-info.yaml, how to model ownership, systems and APIs, and the automation that stops the catalog from drifting out of date.
Published May 14, 20245 min read
A software catalog is only worth opening if people trust it. The first time an engineer looks up who owns a failing service, finds a team that was reorganized away last year, and has to ask in a chat channel anyway, the catalog has lost them.
Backstage makes it easy to load thousands of entities. The harder work is deciding what each entity should say, and making sure it stays correct without depending on anyone remembering to update a YAML file.
Start with the questions it must answer
A catalog is useful when it answers questions people actually ask during the working day and during incidents:
- Who owns this service, and how do I reach them?
- What does it depend on, and what depends on it?
- Where is its code, its documentation, its dashboard, its runbook?
- What APIs does it expose, and who consumes them?
- Is it in production, experimental or being retired?
Everything in the catalog should help answer one of those. Information that changes on every deploy, such as image versions, replica counts or current health, belongs in plugins that read live systems, not in YAML.
The entity model
Backstage ships a small set of entity kinds. Most organizations need only a few of them:
| Kind | Represents | Typical source |
|---|---|---|
Component | A deployable or library: service, website, worker, library | catalog-info.yaml in its repository |
API | An interface a component provides: OpenAPI, AsyncAPI, gRPC, GraphQL | The providing component's repository |
Resource | Infrastructure a component needs: database, queue, bucket | Infrastructure repository or a provider |
System | A group of components and APIs that deliver one capability | A central repository or the owning team's |
Domain | A business area grouping several systems | A central repository |
Group, User | Teams and people | The identity provider, never hand-written |
Resist inventing custom kinds early. A spec.type on a Component, such as service, website or library, covers most distinctions, and the built-in kinds are what plugins understand.
A complete catalog-info.yaml
One file in each repository can describe several entities, separated by ---. A typical service declares its component and the API it provides:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: orders
description: Accepts and tracks customer orders for checkout.
annotations:
github.com/project-slug: platform/orders
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: P7QX2LM
links:
- url: https://grafana.internal/d/orders
title: Dashboard
spec:
type: service
lifecycle: production
owner: group:checkout
system: checkout
providesApis:
- orders-api
consumesApis:
- payments-api
dependsOn:
- resource:orders-db
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: orders-api
description: Order creation and status for web and mobile clients.
spec:
type: openapi
lifecycle: production
owner: group:checkout
system: checkout
definition:
$text: ./openapi.yamlA few fields carry most of the value:
ownermust point at aGroup, not a person. People change teams and leave. A group outlives them and is where alerts and questions should go.systemis what makes the catalog navigable at scale. Two hundred components in a flat list are unreadable. Twenty systems of ten components each are not.providesApisandconsumesApisturn the catalog into a dependency graph. Keep the API definition next to the code with$text, so the published spec is the one the service actually implements.- Annotations connect the entity to other tools: source, docs, on-call, CI, Kubernetes. Each plugin documents the annotation it reads. Add only the ones for plugins you have installed.
Ownership comes from the identity provider
Hand-written Group and User entities go stale the week after they are written. Import them instead. Backstage has entity providers for Microsoft Entra ID through Microsoft Graph, for GitHub organizations and teams, for GitLab groups and for LDAP. Run one on a schedule, and the catalog's teams match the identity provider's.
This changes how reorganizations play out. When a team is renamed or dissolved in the identity provider, every component that pointed at the old group shows an owner that no longer resolves. That is a list of services needing a new owner, generated automatically.
Discovery instead of registration
Registering each repository by hand through the UI does not scale and misses new repositories. Configure a discovery provider to find catalog files on its own:
catalog:
providers:
github:
platform:
organization: platform
catalogPath: /catalog-info.yaml
filters:
branch: main
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }GitLab, Bitbucket and Azure DevOps have equivalent providers. With discovery in place, a repository appears in the catalog when its file lands on main and disappears when the file or repository is removed.
Keeping it accurate
Most catalogs decay in the same ways: fields filled in once and never revisited, services that exist but were never registered, and entities that refer to things that no longer exist. Each has a mechanical fix.
Create the file with the service. When new services come from a software template, the template writes catalog-info.yaml with the owner, system and annotations already filled in. That is one reason a golden path and the catalog belong together: the path is the cheapest place to get catalog data right.
Validate in CI. Check every catalog-info.yaml on pull request: the schema is valid, owner names a group that exists, system exists, lifecycle is from an agreed list. Roadie's backstage-entity-validator handles schema checks. A short script against the catalog API handles the reference checks.
Find the gaps. Compare the list of repositories, or the list of deployed workloads, against the catalog on a schedule. Every deployed service without an entity is a gap to close, and the team that deploys it is easy to find from cloud or cluster labels.
Surface what is broken. Backstage records processing errors on entities and marks entities whose source location disappeared as orphans with the backstage.io/orphan annotation. Review both regularly, or build a page that lists them by owner.
Score completeness by team. The Tech Insights plugin can run checks such as "has an owner", "has TechDocs", "has an on-call annotation" and show results per team. Teams fix what they can see, especially when the list is short.
Mark retirements explicitly. Set lifecycle: deprecated when a service is being retired, then delete the file when it is gone. A catalog full of dead services is as misleading as one with gaps.
Where to start
- Import groups and users from your identity provider before anything else.
- Define the first systems with the teams that own them. Ten well-named systems beat a perfect domain model.
- Add
catalog-info.yamlto the most critical services, with owner, system, lifecycle and the annotations for on-call and docs. - Turn on repository discovery so new files appear without manual registration.
- Add catalog validation to CI and to your service templates.
- Review orphans, processing errors and unowned entities on a fixed schedule, and track the count going down.