Skip to content

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:

KindRepresentsTypical source
ComponentA deployable or library: service, website, worker, librarycatalog-info.yaml in its repository
APIAn interface a component provides: OpenAPI, AsyncAPI, gRPC, GraphQLThe providing component's repository
ResourceInfrastructure a component needs: database, queue, bucketInfrastructure repository or a provider
SystemA group of components and APIs that deliver one capabilityA central repository or the owning team's
DomainA business area grouping several systemsA central repository
Group, UserTeams and peopleThe 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:

catalog-info.yaml
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.yaml

A few fields carry most of the value:

  • owner must point at a Group, not a person. People change teams and leave. A group outlives them and is where alerts and questions should go.
  • system is what makes the catalog navigable at scale. Two hundred components in a flat list are unreadable. Twenty systems of ten components each are not.
  • providesApis and consumesApis turn 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:

app-config.yaml
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

  1. Import groups and users from your identity provider before anything else.
  2. Define the first systems with the teams that own them. Ten well-named systems beat a perfect domain model.
  3. Add catalog-info.yaml to the most critical services, with owner, system, lifecycle and the annotations for on-call and docs.
  4. Turn on repository discovery so new files appear without manual registration.
  5. Add catalog validation to CI and to your service templates.
  6. Review orphans, processing errors and unowned entities on a fixed schedule, and track the count going down.