Module 03 · The portal and its catalog · 60 min · You are: Platform and product
The catalog: teams, systems and a real service
In this module
- You learn: The kinds of entity (Group, User, Domain, System, Component, API, Resource), how refs and relations work, and how a location tells the catalog where to read.
- You do: As the platform team, add the organisation and systems to the portal. As team-checkout, create the
checkoutrepo with itscatalog-info.yaml, OpenAPI file and docs. Then register it and follow the relations. - You end with: The catalog holds Acme's teams, you as a User, two systems, and
checkoutwith its API and database. The example data is gone. Yourcheckoutrepo exists on GitHub.
The catalog is only useful if it is true and connected. Each entity is small YAML, but the fields that point at other entities (owner, system, providesApis, dependsOn) are what make "who owns this, what breaks if it goes down" answerable. You will write one side of each relation and watch the catalog add the other.
Open this module in the interactive course to track progress.
Concepts
- Entity
- One YAML document with
apiVersion,kind,metadata(name, title, annotations) andspec. Kinds you use today: Group, User, Domain, System, Component, API, Resource. - Entity ref
kind:namespace/name, likegroup:default/team-checkout. Fields that imply the kind accept the short form:owner: team-checkout.- Relations
- You write
spec.owner; the catalog storesownedByon the component andownerOfon the group. A ref to an entity that does not exist is stored anyway. - Location
- Where the catalog reads YAML from: a file next to the backend, or a URL in Git. Each location can limit which kinds it may add.
- Annotations
- Keys under
metadata.annotationsthat plugins read:github.com/project-sluglinks the repo,backstage.io/techdocs-refpoints at the docs.
Files to know
Open them in your editor or on GitHub; the bullets say what to look at.
docs/catalog-model.md(reference, your repo): The catalog model in one page, with links to the full reference.portal/catalog/org.yaml(reference, your repo): Teams and people, owned by the platform team.checkout/catalog-info.yaml(reference, your repo): team-checkout's description of its service.
Steps
1. Add the organisation and systems
The platform team owns who exists. Copy the two files and put your GitHub name in as the User.
mkdir -p ~/work/portal/catalog && cp ~/work/backstage-workshop/portal/catalog/org.yaml ~/work/backstage-workshop/portal/catalog/systems.yaml ~/work/portal/catalog/
perl -pi -e 's/YOUR_GITHUB_[U]SER/YOUR_GITHUB_USER/g' ~/work/portal/catalog/org.yaml
2. Point the catalog at them
Replace the example locations. Each location only allows the kinds it should hold.
Change in ~/work/portal/app-config.yaml
Under catalog.locations.
- # Local example data, file locations are relative to the backend process, typically `packages/backend`
- - type: file
- target: ../../examples/entities.yaml
+ # Platform-owned catalog data (module 03). File paths are relative to packages/backend.
+ - type: file
+ target: ../../catalog/org.yaml
+ rules:
+ - allow: [Group, User]
+ - type: file
+ target: ../../catalog/systems.yaml
+ rules:
+ - allow: [Domain, System]
Delete from ~/work/portal/app-config.yaml
- # Local example organizational data
- - type: file
- target: ../../examples/org.yaml
- rules:
- - allow: [User, Group]
Save. The backend reloads its config by itself. The example template location stays for now.
3. Check the teams
In your second terminal (with .env loaded).
Expect acme, platform and team-checkout. The example guests group is gone: its location was removed, so its entities were deleted.
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=group" | jq -r '.[].metadata.name'
The relations the catalog built for team-checkout. Expect childOf group:default/acme, hasMember you, and ownerOf for domain:default/commerce and system:default/shop:
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities/by-name/group/default/team-checkout | jq -r '.relations[] | .type + " " + .targetRef'
4. As team-checkout: create the checkout repo
In a real company this repo holds the code. Here it holds what the portal needs: catalog-info.yaml, an OpenAPI file and docs.
mkdir -p ~/work/checkout && cp -R ~/work/backstage-workshop/checkout/. ~/work/checkout/
perl -pi -e 's/YOUR_GITHUB_[U]SER/YOUR_GITHUB_USER/g' ~/work/checkout/catalog-info.yaml
cd ~/work/checkout && git init -q -b main && git add -A && git commit -qm "checkout: catalog, API and docs"
gh repo create YOUR_GITHUB_USER/checkout --public --source . --push
5. Register it
A URL location: the catalog reads the file from GitHub with the token in .env, and resolves $text: ./openapi.yaml relative to it.
Add to ~/work/portal/app-config.yaml under catalog.locations, after the systems.yaml location
# team-checkout's repo, registered by URL.
- type: url
target: https://github.com/YOUR_GITHUB_USER/checkout/blob/main/catalog-info.yaml
Save, give it half a minute, then list the components and APIs. Expect checkout and checkout-api:
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=component,kind=api" | jq -r '.[].metadata.name'
6. Follow the relations
You wrote four fields in spec; here is what the catalog made of them.
Expect ownedBy group:default/team-checkout, partOf system:default/shop, providesApi api:default/checkout-api and dependsOn resource:default/checkout-db:
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities/by-name/component/default/checkout | jq -r '.relations[] | .type + " " + .targetRef'
Now the UI. Open checkout in the catalog: the Overview tab shows the owner, the system and a relations graph. The APIs tab lists checkout-api; click it and open Definition to see the OpenAPI file rendered. Then click Team Checkout (the owner): its page lists what it owns.
7. Ship it
cd ~/work/portal && git status --short
Expect app-config.yaml modified and the new catalog/ folder.
Propose it as a pull request. Put it on its own branch and push it:
git switch -c catalog-acme && git add -A && git commit -qm "Catalog: Acme teams, systems and checkout"
git push -qu origin HEAD
Open the pull request. It prints a link:
gh pr create --fill
Open that link in your browser. Files changed shows exactly your diff.
Merge it on GitHub (Squash and merge, then Delete branch), or from the terminal:
gh pr merge --squash --delete-branch
Either way, bring your copy up to date:
git switch -q main && git pull -q
Simplified for the course
Teams and people are typed into
org.yaml. In production an org provider (GitHub, Microsoft Entra ID, LDAP) imports them, so leavers disappear on their own.
Break it on purpose: A typo in the owner
team-checkout tidies their catalog-info.yaml and mistypes their own team name. Nothing complains: the push works and the portal keeps running.
You practise: finding a wrong relation when no error exists, and reading relations from both ends.
1. Break it
Change in ~/work/checkout/catalog-info.yaml
In the Component, not the API or Resource.
- owner: team-checkout
- system: shop
- providesApis: [checkout-api]
+ owner: team-chekout
+ system: shop
+ providesApis: [checkout-api]
cd ~/work/checkout && git commit -qam "checkout: tidy catalog-info" && git push -q
The catalog checks the file again every few minutes. Ask it to refresh now (with .env loaded):
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" -X POST -H 'Content-Type: application/json' localhost:7007/api/catalog/refresh -d '{"entityRef":"component:default/checkout"}'
2. What you see
Expected: Team Checkout owns checkout. You get: the Team Checkout page no longer lists checkout under what it owns. There is no error in the portal, in the backend log or on the entity.
The Overview of checkout shows an owner link, but it goes to a page that does not exist.
3. Fix it yourself
Find out who the catalog thinks owns checkout, and why it accepted it. Fix it the way team-checkout would, and prove team-checkout owns checkout again.
Hint 1
Read the relations from both sides: ownedBy on the component, ownerOf on the group. They should agree.
Hint 2
The catalog stores a ref to an entity that does not exist without complaint. Compare the ownedBy target with the group name letter by letter.
The fix
Change in ~/work/checkout/catalog-info.yaml
- owner: team-chekout
+ owner: team-checkout
cd ~/work/checkout && git commit -qam "checkout: fix owner" && git push -q
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" -X POST -H 'Content-Type: application/json' localhost:7007/api/catalog/refresh -d '{"entityRef":"component:default/checkout"}'
Expect ownerOf component:default/checkout among the lines:
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities/by-name/group/default/team-checkout | jq -r '.relations[] | .type + " " + .targetRef'
After you fixed it: the five questions
- What failed?
- The component's owner pointed at a group that does not exist, so the real team lost it.
- How was it detected?
- Only by looking: the team page no longer listed the service. No error anywhere.
- Who responds?
- team-checkout: they own the file and the fix.
- How is it corrected?
- Correct the owner in their repo and refresh (or wait for the next processing loop).
- How is it prevented?
- Create
catalog-info.yamlfrom a template with an owner picker (module 06), and run a CI check that every ref in the file resolves through the catalog API.
Quiz
- You write
spec.owner: team-checkouton a component. What does the catalog store?ownedByon the component andownerOfon the group- Only
ownedByon the component; the group must list it underspec.owns - A copy of the group inside the component
- A component names an owner that does not exist. What happens?
- The catalog rejects the entity and logs a validation error
- The ref is stored and the link leads nowhere; nothing reports it
- The catalog creates an empty group with that name
- Why does
$text: ./openapi.yamlwork for the checkout repo?- The file is copied into the portal repo when you register it
- The frontend fetches it from GitHub when someone opens the API page
- It is resolved relative to the URL location of
catalog-info.yamland read with the GitHub token
Answers
ownedByon the component andownerOfon the group You write one side; the catalog adds both.- The ref is stored and the link leads nowhere; nothing reports it Refs are not checked for existence. Typos survive until someone looks.
- It is resolved relative to the URL location of
catalog-info.yamland read with the GitHub token Processors read$textreferences next to the file they came from.
Field note
The most useful catalog question in any company is "who do I call when this breaks?". If the answer is a person who left two years ago, the portal has failed. That is why I make the owner field a team, never a person.