Cheat notes · Backstage
Backstage: cheat notes
Every module on one page, cut to what you need to remember: the idea, the commands, the pitfalls and a few questions to test yourself. Made for review after you have done the course, not instead of it. Back to the course.
00 Orientation: how to think about Backstage
This module only · Full module
The idea
Backstage is a framework, not a product: your platform team owns a TypeScript app built with it. Every feature hangs off the Software Catalog, so the portal is only as good as the data in it, and that data lives in Git next to the code.
Remember
- A framework:
@backstage/create-appgenerates a monorepo (React frontend, Node backend) that you own, upgrade and ship like any service. - The catalog is the product: who owns what, what depends on what. A wrong catalog makes every page wrong.
- Git is the source: teams describe their software in
catalog-info.yamlnext to their code. The portal reads it; it does not own it. - Config before code: most behaviour is
app-config.yaml(where the catalog reads, how people sign in). Code is for what config cannot do. - Who owns what: the platform team owns the
portalrepo (app, config, image,catalog/with teams and systems, templates, custom actions). team-checkout owns whatcheckoutis: itscatalog-info.yamlanddocs/.
Pitfalls
- Spending weeks on plugins while half the components have no real owner. Rollouts stall on the catalog, not the code.
- Fixing a wrong owner on the page. Pages render catalog data: fix the field in that service's
catalog-info.yaml.
Test yourself
Answer in your head first, then open the answer.
What do you get from @backstage/create-app?
A TypeScript monorepo with a frontend and a backend that your team owns, builds and upgrades.
Where should the facts about checkout (owner, APIs, docs) live?
In the checkout repo next to the code, owned by team-checkout.
A page shows the wrong owner for a service. What is most likely wrong?
The catalog data: the owner field in that service's catalog-info.yaml.
Where module 00 leaves you: Nothing is running yet. Your details are saved in the course, every tool is checked, and the reference repo is on your laptop to copy from.
01 Create the app and find your way around
This module only · Full module
The idea
The generated app is small: one file wires the backend, one wires the frontend, one YAML file configures both, and everything else is npm packages. Learn to go from "I see this in the UI" to "this line adds it".
Remember
- The monorepo:
packages/app(React frontend),packages/backend(Node backend),plugins/(your plugins),app-config.yaml(config for both), tied together by Yarn workspaces. - Backend plugins: one process.
packages/backend/src/index.tsadds each plugin withbackend.add(import(...)); each serves its API under/api/<pluginId>. - Frontend extensions: the app is built from extensions (pages, nav items, entity tabs). Installed plugin packages are discovered (
app.packages: all);App.tsxadds the rest. The Visualizer lists them, e.g.page:catalog. - Ports:
yarn startruns the frontend on 3000 and the backend on 7007. - Every request is authenticated:
curlwithout a token gets401, even on your laptop. - Pinned:
create-app0.9.3 generates Backstage 1.56.0, recorded inbackstage.json.
Pitfalls
- Commenting out a
backend.addline that "looks unused": the portal starts fine, but the feature behind it answers404(here: templates list, but Choose fails, because the list comes from the catalog and running comes from the scaffolder). - Expecting data to survive a restart: the generated app uses in-memory SQLite and example data until module 02.
Commands
npx @backstage/[email protected] --path portal | Generate the app |
yarn start | Start frontend (3000) and backend (7007). Leave this terminal running |
grep -n "target:" app-config.yaml | Where the catalog reads from (catalog.locations) |
grep -n "scaffolder" packages/backend/src/index.ts | Which backend plugins are added |
curl -s -o /dev/null -w "%{http_code}\n" localhost:7007/api/catalog/entities | 401 = alive, wants a token; 404 = plugin missing |
Test yourself
Answer in your head first, then open the answer.
Where is a backend plugin turned on?
A backend.add(import(...)) line in packages/backend/src/index.ts.
Why does curl localhost:7007/api/catalog/entities return 401?
Every backend request needs a token: from a user, another plugin or a configured script.
The template list shows, but Choose fails with 404. Where is the problem?
The backend: the plugin serving /api/scaffolder is not loaded.
Your fastest debugging habit for a failing page?
Open the browser's network tab: /api/<pluginId>/... names the plugin, and 401, 403, 404 each point somewhere different.
Where module 01 leaves you: A portal repo on GitHub with the generated app, running on your laptop at http://localhost:3000 with example data and guest sign-in.
02 Configuration, secrets and Postgres
This module only · Full module
The idea
Everything is configuration: one YAML file in Git with placeholders, the values from the environment. Config files layer (objects merge, lists replace), secrets never reach Git or the browser, and scripts call the API with a configured token.
Remember
- Config layers:
app-config.yaml, thenapp-config.local.yamlif present, or the files given with--config. Later files override keys. Lists are replaced, not merged. - Environment variables:
${POSTGRES_PASSWORD}is read at backend start;${POSTGRES_HOST:-localhost}has a default. A missing value is only an error when a plugin needs it. - Frontend visibility: each plugin's config schema marks what the browser may see. Secrets are never sent.
- External access: scripts and CI call the API with a token in
backend.auth.externalAccess; each token has asubject, so logs show who called. - Postgres: one database per plugin (
backstage_plugin_catalog,backstage_plugin_scaffolder, ...). .envstays on your laptop and is git-ignored.yarn startdoes not read it: load it withset -a && . ./.env && set +ain every terminal.
Pitfalls
- A typo in a variable name on a fresh laptop:
Missing required config value at 'backend.auth.externalAccess[0].options.token'and the backend shuts down. Plainconfig:checkpasses;--strictcatches it. Keep a.env.examplewith every name. - A variable left over in your shell from an earlier session hides the same mistake. Test from a fresh terminal.
- Pasting
config:print --with-secretsoutput anywhere.
Commands
set -a && . ./.env && set +a | Load .env into this terminal |
yarn backstage-cli config:print --frontend | What the browser receives |
yarn backstage-cli config:print | The backend view, secrets shown as <secret> |
yarn backstage-cli config:check --strict | Fail on a missing required value, not only on a wrong shape |
curl -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities | Call the API as a script |
git check-ignore .env | Prove Git ignores the secrets file |
Placeholders in app-config.yaml
backend:
auth:
externalAccess:
- type: static
options:
token: ${PORTAL_API_TOKEN}
subject: portal-cli
database:
client: pg
connection:
host: ${POSTGRES_HOST:-localhost}
password: ${POSTGRES_PASSWORD}
Test yourself
Answer in your head first, then open the answer.
You set catalog.locations in app-config.local.yaml. What happens to the list in app-config.yaml?
It is replaced. Lists in a later file replace the whole list; objects merge.
Why is the database password safe in app-config.yaml?
The file only holds ${POSTGRES_PASSWORD}. The value comes from the environment and is never sent to the browser.
What does config:check --strict add?
It fails when a required value is missing, not only when the shape is wrong.
Where module 02 leaves you: The portal is called Acme Portal, stores its data in Postgres in Docker, and accepts a static API token from .env. The config change is merged in your portal repo; .env is not in Git.
03 The catalog: teams, systems and a real service
This module only · Full module
The idea
The catalog is small YAML entities connected by refs. You write one side of a relation (owner, system, providesApis, dependsOn) and the catalog stores both sides, which is what makes "who owns this, what breaks if it goes down" answerable.
Remember
- Entity:
apiVersion,kind,metadata(name, title, annotations),spec. Kinds here: Group, User, Domain, System, Component, API, Resource. - Entity ref:
kind:namespace/name, e.g.group:default/team-checkout. Fields that imply the kind take the short form:owner: team-checkout. - Relations:
spec.owneron a component storesownedByon it andownerOfon the group. Refs are not checked: one to a missing entity is stored anyway. - Location: where the catalog reads YAML: a file next to the backend (paths relative to
packages/backend) or a URL in Git. Each location can limit kinds withrules: - allow: [...]. - Annotations plugins read:
github.com/project-sluglinks the repo;backstage.io/techdocs-refpoints at the docs.$text: ./openapi.yamlis resolved relative to the file's location. - Removing a location deletes its entities (the example
guestsgroup vanished).
Pitfalls
- A typo in the owner (
team-chekout): no error anywhere. The team page no longer lists the service and the owner link leads to a missing page. Use an owner picker in templates and a CI check that every ref resolves. - Making a person the owner. People leave; make the owner a team.
Commands
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=group" | jq -r '.[].metadata.name' | List entities of one kind |
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities/by-name/component/default/checkout | jq -r '.relations[] | .type + " " + .targetRef' | The relations the catalog built |
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"}' | Refresh one entity now |
A component that is connected
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: checkout
annotations:
github.com/project-slug: OWNER/checkout
backstage.io/techdocs-ref: dir:.
spec:
type: service
lifecycle: production
owner: team-checkout # -> ownedBy / ownerOf
system: shop # -> partOf
providesApis: [checkout-api] # -> providesApi
dependsOn: [resource:checkout-db]
Test yourself
Answer in your head first, then open the answer.
You write spec.owner: team-checkout. What does the catalog store?
ownedBy on the component and ownerOf on the group.
A component names an owner that does not exist. What happens?
The ref is stored, the link leads nowhere, and nothing reports it.
Why does $text: ./openapi.yaml work for the checkout repo?
It is resolved relative to the URL of catalog-info.yaml and read with the GitHub token.
Where module 03 leaves you: The catalog holds Acme's teams, you as a User, two systems, and checkout with its API and database. The example data is gone. Your checkout repo exists on GitHub.
04 Catalog at scale: let it find the repos
This module only · Full module
The idea
One URL per repo does not scale. A discovery provider scans your GitHub account on a schedule and reads every catalog-info.yaml it finds, so adding the file is the whole onboarding, and catalog rules stop team repos from inventing teams or users.
Remember
- Entity provider vs location: the catalog reads locations in its processing loop; a provider runs on its own schedule and writes entities in (GitHub discovery, org imports).
- A backend module, not a plugin:
plugin-catalog-backend-module-githubplus onebackend.addline plugs into the catalog plugin. A new backend module needs a restart. catalog.providers.github.<id>:organization,catalogPath,filters(branch, repository),schedule. The id names the scheduled task:github-provider:<id>:refresh.- Rules:
catalog.ruleslists what any source may add (Component, System, API, Resource, Location). Only the platform's own file locations widen it (Group, User, Domain, Template). Anything else is refused with a warning in the log. - Orphans: an entity its source no longer mentions. The default strategy
deleteremoves it in a cleanup every 30 seconds. - Discovery reads
main: acatalog-info.yamlonly counts once it is merged.
Pitfalls
- A team defining its own Group in its repo: the group never appears and nothing on the entity page says why. The log says
not of an allowed kind for that location. Groups go tocatalog/org.yamlby pull request. - Leaving
create-app's owncatalog-info.yamlowned by[email protected]: discovery finds it. Make it true before you merge.
Commands
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github | Install the discovery module |
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" -X POST localhost:7007/api/catalog/.backstage/scheduler/v1/tasks/github-provider:teamRepos:refresh/trigger | Run the scan now |
... | jq -r '.metadata.annotations["backstage.io/managed-by-origin-location"]' | Where an entity now comes from |
Provider and rules
catalog:
rules:
- allow: [Component, System, API, Resource, Location]
providers:
github:
teamRepos:
organization: YOUR_GITHUB_USER
catalogPath: /catalog-info.yaml
filters: { branch: main, repository: '.*' }
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
Test yourself
Answer in your head first, then open the answer.
What is the difference between a location and an entity provider?
The catalog reads a location; a provider runs on its own schedule and writes entities in.
You remove a location. What happens to its entities by default?
They become orphans and are deleted within about 30 seconds.
Why can a discovered team repo not add a User?
catalog.rules does not allow User for general sources; only the platform's org.yaml location does.
Where module 04 leaves you: The catalog finds checkout and portal by scanning your GitHub account. Team repos may add components, APIs and resources, but not groups or users.
05 Sign in with GitHub
This module only · Full module
The idea
Sign-in turns a GitHub login into a catalog User, and the User into groups. The resolver is the lock: no matching User entity, no sign-in. The catalog is the source of who you are.
Remember
- Auth provider: a backend module that talks to an identity provider, configured under
auth.providers.<name>.<environment>(auth.environmentpicks which). - Sign-in resolver: maps the provider identity to a User entity.
usernameMatchingUserEntityNamelooks for a User named like the GitHub username. - Ownership entities: the user plus the groups they are a member of, put in the token. "Owned" filters and permissions read them.
- OAuth callback on port 7007:
http://localhost:7007/api/auth/github/handler/frame. The backend finishes the flow. - Swap the sign-in page with a frontend module:
createFrontendModule({ pluginId: 'app', extensions: [SignInPageBlueprint.make(...)] }), added tofeaturesinApp.tsx. - A door, not a lock: with the allow-all permission policy every signed-in user can still do everything.
Pitfalls
- Renaming a User in
org.yamlto a "nicer" name: the next sign-in fails withunable to resolve user identity. Import users with an org provider and revieworg.yamlwith CODEOWNERS. - Debugging OAuth first when login breaks. Most login problems are catalog problems: a missing, renamed or unimported User.
- Forgetting to restart: the portal reads new environment variables only at start.
Commands
backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); | Add the GitHub provider to the backend |
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=user" | jq -r '.[].metadata.name' | The User names sign-in can resolve to |
The provider and its resolver
auth:
environment: development
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
signIn:
resolvers:
- resolver: usernameMatchingUserEntityName
Test yourself
Answer in your head first, then open the answer.
What does the sign-in resolver decide?
Which catalog User a provider identity becomes, and so whether the person may sign in at all.
Why is the OAuth callback URL on port 7007?
The auth backend plugin finishes the OAuth flow, and the backend listens on 7007.
How did you replace the sign-in page?
With a frontend module that overrides the app plugin's sign-in page extension.
Where module 05 leaves you: The portal only lets in people who have a User entity. You sign in with GitHub and the portal knows you are in platform and team-checkout.
06 Golden paths with software templates
This module only · Full module
The idea
A template makes the right way the easy way: a form (parameters), actions run on the backend (steps), and links (output). The service starts with an owner, a system, docs and a catalog entry from its first commit.
Remember
- Parameters: JSON Schema with
ui:hints, one form page per item. Each property becomesparameters.<name>. Pickers (OwnerPicker,EntityPicker,RepoUrlPicker) read the catalog. - Steps: each runs one action on the backend with the backend's credentials:
fetch:templaterenders the skeleton,publish:githubcreates the repo,catalog:registeradds it. Outputs feed later steps:steps['publish'].output.repoContentsUrl. - Skeleton: files with
${{ values.x }}placeholders, rendered with Nunjucks byfetch:template. - Templates are entities: registered through a location that allows
Template, owned by the platform team. - Dry run: the template editor (Create, ⋮, Template Editor) runs steps without side effects and shows files, log and output. Actions without dry-run support are skipped.
Pitfalls
- Removing a value from the
fetchstep that "looks unused": the template still runs, and the rendered file hassystem:with nothing after it. Dry-run every template in CI and assert key fields. - Shipping a clever first template nobody needs. Start with the service type teams create most often.
Commands
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=template" | jq -r '.[].metadata.name' | Which templates the catalog has |
gh repo view OWNER/cart --json name,description | The repo the template created |
The skeleton of a template
spec:
parameters:
- title: About the service
required: [name, owner]
properties:
owner:
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter: { kind: Group, spec.type: team }
steps:
- id: fetch
action: fetch:template
input:
url: ./skeleton
values:
owner: ${{ parameters.owner }}
- id: publish
action: publish:github
- id: register
action: catalog:register
input:
repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
Test yourself
Answer in your head first, then open the answer.
Where do template steps run?
On the backend, with the backend's credentials, one action per step.
What does a dry run in the template editor do?
Runs the steps without side effects and shows the rendered files, the log and the output.
Why does the owner field use OwnerPicker with a catalogFilter?
It only offers teams that exist, so an owner typo is impossible.
Where module 06 leaves you: The portal offers one template, Node.js service. A cart repo exists on GitHub, created by the template and already in the catalog, owned by team-checkout.
07 Your own scaffolder action
This module only · Full module
The idea
Company rules need your own code, and the smallest piece of backend code is a module: it plugs into one plugin (the scaffolder) through its extension point and gets services like the catalog injected.
Remember
- Backend module:
createBackendModule({ pluginId: 'scaffolder', moduleId }), added withbackend.add.yarn new --select scaffolder-backend-modulegenerates and wires it. - Extension point: what a plugin offers modules.
scaffolderActionsExtensionPoint.addActions(...)registers actions. - Service refs in
deps:catalogServiceRef,loggerServiceRef,databaseServiceRef; the backend injects them. - The action:
createTemplateAction({ id, description, schema: { input, output }, handler }). Schemas are Zod; the Installed Actions page shows them.ctx.output(...)feeds later steps. - On whose behalf: read the catalog with
ctx.getInitiatorCredentials(), so permissions apply to the user running the template. - Dry run:
supportsDryRun: trueonly for read-only actions. An action that writes checksctx.isDryRunand skips the write.
Pitfalls
- Deleting
supportsDryRun: true: tests and types still pass, but the dry run logsSkipping because acme:team:lookup does not support dry-runand the README saysOwned by ****. Test for it. - Throwing a generic
Errorfor bad input: useInputErrorso the user sees a clear message, and fail before anything is created (put the lookup step first).
Commands
yarn new --select scaffolder-backend-module --option moduleId=acme | Generate the module and wire it into the backend |
cd plugins/scaffolder-backend-module-acme && yarn test --watchAll=false | Run its tests |
yarn tsc | Type-check the whole repo |
A module that adds an action
export const scaffolderModuleAcme = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'acme',
register({ registerInit }) {
registerInit({
deps: {
scaffolderActions: scaffolderActionsExtensionPoint,
catalog: catalogServiceRef,
},
async init({ scaffolderActions, catalog }) {
scaffolderActions.addActions(createTeamLookupAction({ catalog }));
},
});
},
});
Test yourself
Answer in your head first, then open the answer.
How does your module get the catalog client?
It lists catalogServiceRef in deps and the backend injects it.
Why is the action a module of the scaffolder and not its own plugin?
It extends the scaffolder through its extension point and has no API of its own.
Why read the catalog with ctx.getInitiatorCredentials()?
The read happens as the user who runs the template, so permissions apply to them.
Where module 07 leaves you: The scaffolder has your acme:team:lookup action with three passing tests. New services say "Owned by Team Checkout" in their README and docs.
08 Docs next to the code with TechDocs
This module only · Full module
The idea
Docs live in the repo, are built by MkDocs and shown on the entity page next to the owner and the API, so they change in the same pull request as the code. One annotation links the entity to its docs.
Remember
backstage.io/techdocs-ref: dir:.points at the folder withmkdocs.yml, relative tocatalog-info.yaml. MkDocs then readsdocs_dir(defaultdocs).- Builder and generator: the backend fetches the repo and runs MkDocs.
runIn: dockeruses thespotify/techdocsimage;runIn: localneeds MkDocs installed. - Publisher: where built sites are stored: the local disk here, a bucket (S3, GCS, Azure) in production.
- Recommended in production: build in each repo's CI with
techdocs-cli generateand publish to a bucket; the portal only reads. - Templates give docs from day one: the skeleton includes
mkdocs.ymlanddocs/.
Pitfalls
- "Fixing" the annotation to
dir:./docsbecause the Markdown is there:The path '/input/docs' isn't an existing directory. It must point at themkdocs.ymlfolder. - Making docs builds the portal's problem: a broken build should fail the pull request in the repo's CI.
Commands
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" -N localhost:7007/api/techdocs/sync/default/component/checkout | Rebuild one entity's docs now and stream the log |
docker images spotify/techdocs | The image that built them |
Test yourself
Answer in your head first, then open the answer.
What does backstage.io/techdocs-ref: dir:. point at?
The folder with mkdocs.yml, relative to catalog-info.yaml.
Why does the portal need Docker for docs on your laptop?
With runIn: docker the generator runs MkDocs in the spotify/techdocs image.
What is the recommended production setup?
Build in each repo's CI, publish to a bucket; the portal only reads.
Where module 08 leaves you: Both services have a TechDocs tab with their docs. You know where the build runs and what the annotation does.
09 Ship it: an image, production config and an upgrade
This module only · Full module
The idea
Production is one Node process that serves the built frontend and the API, configured by environment variables. Whatever the portal reads from disk (catalog files, templates) or runs (MkDocs) must be in the image. Upgrades are part of shipping: small and monthly.
Remember
- Host build:
yarn build:backendbuilds frontend and backend on your machine; the Dockerfile only copies the result in.yarn build-imagetags itbackstage. - One port: in production the backend serves the frontend, so
app.baseUrlandbackend.baseUrlare the same URL (7007). - Config layering: the container starts with
--config app-config.yaml --config app-config.production.yaml. Objects merge; lists such ascatalog.locationsare replaced, so the production file repeats every location (paths relative to/app). - What goes in the image:
COPYthecatalogandtemplatesfolders, and install MkDocs (mkdocs-techdocs-core) forrunIn: local; there is no Docker in the container. - A release is a set of package versions:
yarn backstage-cli versions:bump --release Xmoves every@backstage/*package together. Check the upgrade helper for changes to generated files.
Pitfalls
- Trimming a
COPYline that "only copies YAML": sign-in fails withunable to resolve user identitybecause the catalog has no Users. The log saysfile /app/catalog/org.yaml does not exist. - Testing an image against an old database: entities survive there and hide what is missing. Smoke-test against an empty database.
- Giving Docker 2 GB: the build runs out of memory. Give it at least 4 GB.
Commands
yarn install --immutable && yarn tsc && yarn build:backend | Build on the host |
yarn build-image | Build the backstage image |
docker run --rm -p 7007:7007 -e POSTGRES_HOST=... -e PORTAL_API_TOKEN ... backstage | Run it; variables without = pass through from your shell |
yarn backstage-cli versions:bump --release 1.56.3 | Upgrade every Backstage package to one release |
jq -r .version backstage.json | Which release the app is on |
Test yourself
Answer in your head first, then open the answer.
You list catalog.locations in app-config.production.yaml. What happens to the ones in app-config.yaml?
They are replaced. The production list is the only list.
Why did the broken image work fine against the old database?
Entities stay in the database; only a fresh database showed the gap.
How should you upgrade Backstage?
Run versions:bump regularly, check the upgrade helper, then build and test.
Where module 09 leaves you: A backstage image that runs the whole portal on one port, with your catalog, template and docs, on Backstage 1.56.3.
10 Capstone: onboard team-payments
This module only · Full module
The idea
Onboarding a team is what a portal is for: the platform adds a Group and a System by pull request, the team creates its service from the template, and the image proves it all works from Git alone.
Remember
- Platform: in
catalog/org.yamladd theteam-paymentsGroup (parentacme), add it toacme'schildren, add yourself viamemberOf; incatalog/systems.yamladd thepaymentsSystem (owner team-payments, domain commerce). - Team: Create → Node.js service, owner Team Payments, system payments. The template and discovery do the rest; the team needs nothing from the platform.
- Prove it: rebuild the image and run it against a fresh database;
payments-apimust showownedBy group:default/team-paymentsandpartOf system:default/payments. - Check acceptance criteria yourself before opening the solution.
Pitfalls
- Adding the Group but not the parent's
childrenor yourmemberOf: relations look half-built and you cannot pick the team as yours. - Typing people into YAML for every team: the next step is an org provider that imports teams and users.
Commands
curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities/by-name/component/default/payments-api | jq -r '.relations[] | .type + " " + .targetRef' | The new service's relations |
docker rm -f portal-prod-db && docker network rm portal-net | Tear down |
Test yourself
Answer in your head first, then open the answer.
What did onboarding team-payments need from the platform team?
A Group, a System and a membership in org.yaml and systems.yaml, through a pull request.
What did team-payments need from the platform team to create a service?
Nothing: the template and discovery do the rest.
What would you automate next with a third team?
Importing teams and users with an org provider (GitHub or your identity provider).
Where module 10 leaves you: team-payments exists, owns payments-api and the payments system, and you are a member. The image runs it all.