TT Lab
Get started
Learn Learning paths Courses

CBA — Backstage Associate

Passing the Schema Does Not Mean It Is Valid

Continue in TT Lab

In one line

A catalog filters in two layers, schema and policy. An entity that passes only the schema merely has the right shape, so it gets in even with a space in its name or with spec misspelled.

Why you need to look at both layers

In the earlier module you wrote several catalog-info.yaml files. But the place where that lab runs has no Backstage backend, so there was no way to check whether those YAML files actually pass.

This lab does not bring up Backstage as a whole — that takes hundreds of MB and several minutes of build. Instead it runs directly the very libraries used when putting things into the catalog.

Schema and policy catch different things

The catalog filters in two layers.

스키마   형태 — apiVersion·kind·필수 필드가 있는가
정책     규칙 — 이름 형식, 63자 제한, 모르는 루트 필드 거부

If you use only entitySchemaValidator, you see only the schema. So it passes even with a space in the name, or with spec mistyped as spce. The real catalog puts a policy on top of that.

The reason for blocking unknown root fields is especially valuable. If a spce typo passes, that service goes into the catalog without a spec — with neither owner nor lifecycle. It is the same spirit as CNPA's structural schema pruning.

References create relations

owner: team-a is actually a reference that points to group:default/team-a. These references gather into a graph, and everything Backstage shows on screen as "who owns it and what it depends on" is the result of walking this graph. If a reference breaks, the relation breaks — that is why the name format is strict.

Configuration is merged deeply key by key

It is not that the last file overwrites everything. For scalars the later one wins, keys present on only one side survive, and arrays are replaced wholesale. This array rule is the trap.

Where to validate

When you find out what the catalog rejects decides the developer experience. Even for the same error, the cost differs completely depending on when you notice it.

Most organizations have only the third, and that is why the list quietly goes stale. The cost of moving validation into CI is almost nothing. You call the same library this lab uses from a script and report failure with the exit code, and you are done. And that script must go into the new-repository template too, so that it follows automatically into repositories created later.

How a catalog rots

A developer portal fails not because it lacks features but because the list diverges from reality. A list nobody trusts is a list nobody looks at, and a list nobody looks at goes stale faster. There are only a few places where this vicious cycle starts.

The owner is empty or points to a team that no longer exists. When the organization is reshuffled, group names change while the entities stay. An entity with a broken reference floats on screen with its relations cut, and when an outage happens you cannot tell whom to contact. Counting ownerless entities regularly and keeping that number as a metric is the cheapest defense.

Entries remain even after the service is gone. If you delete the repository but the catalog keeps what it last read, a nonexistent service keeps appearing in the list. You must set up ingestion so that the entry disappears when the source disappears, and manually registered entries are especially not free of this problem.

Registered but nobody ever opens it. For a catalog to have value, you must be able to get from an entry to what you actually need: the dashboard, the on-call contact, recent deployments, the documentation. Without these links, a catalog ends up as a table with only names and owners.

That is why mature teams treat the catalog as a target of inspection. They periodically check whether required fields are filled in, whether the owner is an existing group, and whether the lifecycle value is within the defined list, and they make the template put a correct catalog-info.yaml in when a new repository is created. If you leave people to write it by hand every time, the formats become different from person to person, and once those differences pile up, aggregating anything from the list becomes impossible.

What really matters in practice

Always turn on the policy that rejects unknown root fields. If a typo that writes spec as spce passes, that service goes into the catalog with neither owner nor lifecycle. Piling up entries that were registered successfully but have an empty owner is the most common way a list rots.

The reason the reference format is strict is that it is a relationship graph. owner: team-a is a reference that points to group:default/team-a, and everything in "who owns it and what it depends on" on screen is the result of walking this graph. If a reference breaks, the relation is cut entirely.

In configuration merging, arrays are replaced wholesale. For scalars the later one wins and keys on only one side survive, but only arrays follow a different rule. If you write an array halfway in a per-environment file, all the entries of the earlier file disappear.

In the next lab you check these with the real library, getting rejected yourself along the way.