Backstage
Backstage is an open framework — a CNCF project, originally built at Spotify — for building an internal developer portal: one web front door where every service, API, database and team is catalogued, where the docs live next to the code that produced them, and where “start a new service the right way” collapses from a week of copy-pasting into a three-field form and a Create button. It solves the platform problem no amount of clever infrastructure fixes on its own: your golden paths are invisible, your software is undiscoverable, and nobody can answer “what is this thing, who owns it, and where are its docs?”
Imagine a giant toy factory where a thousand people built a thousand toys over ten years, and nobody knows what already exists, who made each toy, or how to build a new one properly. Backstage is the shop window plus the catalogue for that factory. Every toy has a card — what it is, who looks after it, where its instruction sheet is — and the card writes itself from a little note the maker leaves in the box. Along one wall there are big friendly buttons: “Make a new toy.” Press one, type a name, and thirty seconds later there’s a brand-new toy on the shelf, built exactly the way the grown-ups agreed toys should be built. Backstage doesn’t make the toys. It’s the window, the catalogue, and the buttons.
What Backstage is and the problem it solves
☺ Like you’re 10: It’s a website that lists everything your company has built and gives you buttons to build new things the approved way.
Spotify open-sourced Backstage in 2020 and donated it to the CNCF, where it is an incubating project. It is not a product you install and configure; it is a framework. You generate an application from a template, it lands in your Git organisation as a TypeScript monorepo (a React frontend and a Node.js backend), and from then on it is your application — you add plugins, you build it, you deploy it, you upgrade it. That single fact drives most of what is good and most of what is painful about Backstage, so hold onto it.
The problem before a portal
A platform team spends a year building excellent capability. GitOps reconciles everything, Crossplane turns a cloud database into nine lines of YAML, policy is enforced at admission — and developer satisfaction does not move, because none of it is discoverable. Dot still asks in Slack which repo owns the payments API, still copies last year’s Dockerfile from an abandoned service, and still opens a ticket because she does not know the paved road exists. The bottleneck stopped being capability and became findability. That is Backstage’s territory.
Three things in one application
Backstage bundles three cores that reinforce each other. The Software Catalog is a graph of everything you own — components, APIs, resources, systems, teams — built from small YAML files beside the code. Software Templates (run by the scaffolder) turn a golden path into a button that creates a repo, fills it with the right skeleton, and registers the result back into the catalog. TechDocs renders each repo’s Markdown into browsable docs inside the portal. Everything else — Kubernetes views, Argo CD status, on-call, cost — arrives as plugins hanging off catalog entities.
Both get shortened to “IDP,” and confusing them is the most expensive mistake teams make here. The internal developer platform is the paved road itself: clusters, GitOps controllers, control planes, policy, pipelines, secrets. The internal developer portal is the UI in front of it. Backstage is the portal. A portal with no paved road behind it is a restaurant menu with no kitchen — beautiful buttons, nothing gets cooked. Build the road first; Backstage makes it visible and clickable. See Self-Service & Developer Portals.
Where it fits in a platform
☺ Like you’re 10: Backstage sits right at the front, where people are. All the machinery it talks to sits behind it.
In the plane model, Backstage is almost purely developer control plane — the interface layer. It holds very little truth of its own: the catalog is a cache of YAML that lives in your repos, the scaffolder’s output is a Git commit, and the plugins are read-only windows onto systems that would work fine without it. That is a feature. If Backstage is down for an afternoon, deployments keep reconciling and pods keep running; developers just lose their map.
Its neighbours
Draw the arrows and the role gets obvious. Backstage’s scaffolder writes to Git, which Argo CD or Flux then reconciles — the portal never touches a cluster to deploy anything. It reads from Kubernetes to show Dot her pods, from Argo CD for sync status, from Prometheus/Grafana for dashboards, from OpenCost for spend, and from PagerDuty for on-call. The abstractions behind its buttons are usually Crossplane claims or other custom resources — the template drops a nine-line claim into a repo and the control plane does the rest.
CNPE domain relevance
Backstage is not on the official CNPE tool list, but it is the canonical example for the exam’s largest conceptual theme: self-service, golden paths and developer experience. Expect concept questions — portal versus platform, what a software catalog is for, what a scaffolder template does — rather than a task that makes you run a Node app. Pair this page with Developer Experience and Platform as a Product.
“Before the portal I had four browser tabs and a Slack search. Now the payments service has one page: what it is, who owns it, its API spec, its runbook, whether Argo says it’s synced, and who’s on call tonight. I didn’t learn a new tool — I learned a URL. And when I need a new service I don’t read a wiki page from 2022, I press a button and get a repo that already has CI, a namespace and dashboards attached.”
How it works — architecture and the entities it introduces
☺ Like you’re 10: It’s a website (the bit you see) plus a helper program (the bit that reads your repos and remembers everything in a database).
A Backstage deployment is one app with two halves. The frontend is a React single-page app assembled from plugin packages; the backend is a Node.js service (default port 7007) hosting backend plugins — catalog, scaffolder, techdocs, auth, permissions, search — behind a common router at /api/<plugin>. Behind that sits a relational database: SQLite for local development, PostgreSQL for anything real. Backstage does not require Kubernetes at all; it is a container you can run anywhere, and most teams run it on the platform it fronts.
The Software Catalog and its entity kinds
The catalog is a graph whose nodes are entities, each a YAML document with the familiar Kubernetes-style envelope — apiVersion, kind, metadata, spec — under apiVersion: backstage.io/v1alpha1. The file is conventionally catalog-info.yaml at the root of the repo it describes, which is the whole trick: the description ships with the code, so it stays true.
| Kind | What it represents | Key spec fields |
|---|---|---|
Component | A piece of software you build and run — a service, website, library | type, lifecycle, owner, system, providesApis, consumesApis, dependsOn |
API | The interface a component exposes — the contract, not the code | type (openapi, asyncapi, graphql, grpc), lifecycle, owner, definition |
Resource | Infrastructure a component needs — database, bucket, queue, cluster | type, owner, system, dependsOn |
System | A bundle of components, APIs and resources that work as one product | owner, domain |
Domain | A business area grouping related systems (“payments”, “search”) | owner |
Group | A team or org unit — the thing that owns other entities | type, profile, parent, children, members |
User | A person, usually ingested from your identity provider | profile, memberOf |
Location | A pointer to other catalog files — how ingestion fans out | type, target / targets |
Template | A scaffolder golden path (its own group: scaffolder.backstage.io/v1beta3) | type, owner, parameters, steps |
Relations, annotations, and how the graph builds itself
You never write relations; the catalog derives them. Write spec.owner: payments-team on a component and Backstage creates an ownedBy edge to that Group and the reciprocal ownerOf edge back. The same pairing applies to partOf/hasPart, providesApi/apiProvidedBy, consumesApi/apiConsumedBy, dependsOn/dependencyOf, and memberOf/hasMember. That reciprocity makes the portal navigable in both directions: from a team to everything it owns, and from a broken API to everyone who consumes it.
Annotations are the other half of the magic — free-form key/value pairs under metadata.annotations that plugins look for. backstage.io/techdocs-ref tells TechDocs where the Markdown lives; backstage.io/kubernetes-id or backstage.io/kubernetes-label-selector tells the Kubernetes plugin which workloads belong to this entity; argocd/app-name lights up the Argo CD tab. Two are set automatically and matter for debugging: backstage.io/managed-by-location (which file produced this entity) and backstage.io/source-location (where its code lives).
Discovery — how entities get in
Three mechanisms, in increasing order of maturity. Manual registration: a human pastes a URL into “Register existing component,” creating a Location. Static locations: URLs listed under catalog.locations in app-config.yaml — good for org data and templates. Entity providers: catalog.providers.github (with equivalents for GitLab, Bitbucket, Azure DevOps and AWS, plus org-data providers such as LDAP and Microsoft Graph) crawl a whole organisation on a schedule, ingesting every repo that contains a catalog-info.yaml. Providers are the only option that scales, because they also remove entities when the file disappears.
The resources you will actually write
☺ Like you’re 10: Three little files: a card for your service, a form for making new services, and the settings file for the portal itself.
Backstage introduces no Kubernetes CRDs. Its “resources” are YAML files in Git that the backend reads. Three of them carry almost all the weight.
1 · catalog-info.yaml — the card for one service
This is the file every service repo should have. Multiple entities can live in one file, separated by ---, which is how a service, its API and its database land together.
# catalog-info.yaml — lives at the ROOT of the checkout service repo
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: checkout # unique per kind + namespace; lowercase by convention
title: Checkout Service # optional pretty name for the UI
description: Takes a cart and turns it into an order.
tags: [go, payments, tier-1]
annotations:
backstage.io/techdocs-ref: dir:. # TechDocs builds from ./mkdocs.yml
backstage.io/kubernetes-id: checkout # k8s plugin matches this label
argocd/app-name: checkout-prod # Argo CD tab shows sync + health
pagerduty.com/service-id: PXXXXXX # entity YAML is literal — no ${ENV} expansion
github.com/project-slug: acme/checkout
links:
- url: https://grafana.acme.internal/d/checkout
title: Dashboard
icon: dashboard
spec:
type: service # service | website | library
lifecycle: production # experimental | production | deprecated
owner: group:default/payments-team # → derives ownedBy / ownerOf
system: commerce # → derives partOf / hasPart
providesApis: [checkout-api] # → derives providesApi / apiProvidedBy
consumesApis: [inventory-api]
dependsOn:
- resource:default/checkout-db # → derives dependsOn / dependencyOf
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: checkout-api
spec:
type: openapi
lifecycle: production
owner: group:default/payments-team
system: commerce
definition:
$text: ./openapi.yaml # rendered as browsable API docs
---
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: checkout-db
description: Postgres 15, provisioned by a Crossplane claim in this repo.
spec:
type: database
owner: group:default/payments-team
system: commerce☺ Like you’re 10: One file says “here’s me, here’s my door handle, and here’s the box I keep my stuff in” — and the portal draws the picture from that.
spec.owner is a string reference, and if the Group it names is not in the catalog you get a dangling owner reference — the ownership relation is still recorded, but it points at an entity that was never ingested, so the link goes nowhere. Ingest your org (Groups and Users) first, from your identity provider or a checked-in org.yaml, then ingest components. Prefer the fully-qualified group:default/payments-team over the bare payments-team. And keep ownership honest — an unowned service in the catalog is exactly the anti-pattern a portal is supposed to kill.
2 · template.yaml — turning a golden path into a button
A Software Template has two halves. spec.parameters is JSON Schema rendered as a form — enum becomes a dropdown, pattern becomes validation, and ui:field widgets like OwnerPicker, RepoUrlPicker and EntityPicker become pickers wired to the catalog. spec.steps is an ordered list of actions the backend runs server-side, each able to read earlier outputs via ${{ steps.<id>.output.<field> }}.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: golden-path-service
title: New Service (Golden Path)
description: Repo + CI + namespace + database, wired to the paved road.
tags: [recommended, go]
spec:
owner: group:default/platform-team
type: service
parameters: # ── the form Dot fills in ──
- title: Service details
required: [name, owner]
properties:
name:
title: Service name
type: string
pattern: '^[a-z][a-z0-9-]{2,29}$'
owner:
title: Owning team
type: string
ui:field: OwnerPicker # picks a Group from the catalog
ui:options: { catalogFilter: { kind: Group } }
size:
title: Database size
type: string
default: small
enum: [small, medium, large] # renders as a dropdown
- title: Repository
required: [repoUrl]
properties:
repoUrl:
title: Location
type: string
ui:field: RepoUrlPicker
ui:options: { allowedHosts: [github.com], allowedOwners: [acme] }
steps: # ── what runs on submit ──
- id: fetch
name: Render the skeleton
action: fetch:template
input:
url: ./skeleton # the directory next to this file
values: # substituted into ${{ values.* }}
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
size: ${{ parameters.size }}
- id: publish
name: Create the repository
action: publish:github
input:
repoUrl: ${{ parameters.repoUrl }}
defaultBranch: main
repoVisibility: internal
access: acme/payments-team # admin collaborator: an org/team SLUG,
# not the group:default/... entity ref
- id: register
name: Register in the catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output: # links shown on the success page
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Open in catalog
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}Beside that file sits the skeleton directory — an ordinary project tree with placeholders. skeleton/catalog-info.yaml holds name: ${{ values.name }}; skeleton/db-claim.yaml a Crossplane claim with size: ${{ values.size }}; skeleton/.github/workflows/ci.yaml your standard pipeline. fetch:template renders every file and drops the result into the working directory, so the new repo is born already on the paved road: catalogued, documented, built by CI, deployed by GitOps.
A golden path written on a wiki decays the moment someone forgets to read it. The same path as a scaffolder template is enforced by construction: nobody can create a service lacking CI, ownership, docs or dashboards, because the form is the only way to create one. Fix the path once in the template and every service created afterwards inherits the fix. That is the difference between documentation and a platform capability.
3 · app-config.yaml — the portal’s own settings
One YAML file configures the whole app, with app-config.local.yaml and app-config.production.yaml layered on top and ${ENV_VAR} substitution throughout. Catalog, discovery, integrations, TechDocs and every plugin read their settings from here.
app:
baseUrl: https://backstage.acme.internal
backend:
baseUrl: https://backstage.acme.internal
listen: { port: 7007 }
database:
client: pg # SQLite is DEV ONLY — data vanishes
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
integrations:
github:
- host: github.com
apps:
- $include: github-app-credentials.yaml # a GitHub App, not a PAT
catalog:
rules:
- allow: [Component, API, Resource, System, Domain, Group, User, Location]
locations:
- type: url # org data FIRST, so owners resolve
target: https://github.com/acme/platform-config/blob/main/catalog/org.yaml
- type: url # golden-path templates
target: https://github.com/acme/software-templates/blob/main/golden-path-service/template.yaml
rules: [ { allow: [Template] } ]
providers: # needs the GitHub catalog backend module
github:
acmeOrg: # auto-discovery across the whole org
organization: acme
catalogPath: /catalog-info.yaml
filters: { branch: main, repository: '.*' }
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
techdocs:
builder: 'external' # built in CI, NOT on request
generator: { runIn: 'docker' }
publisher:
type: 'awsS3'
awsS3: { bucketName: acme-techdocs }
kubernetes: # powers the Kubernetes tab
serviceLocatorMethod: { type: 'multiTenant' }
clusterLocatorMethods:
- type: 'config'
clusters:
- name: prod-eu
url: https://k8s-prod-eu.acme.internal
authProvider: 'serviceAccount'
serviceAccountToken: ${K8S_PROD_EU_TOKEN} # read-only SA, please☺ Like you’re 10: This is the portal’s own settings menu — where its database is, which GitHub org to scan, and which clusters it may peek at.
TechDocs — docs that live with the code
TechDocs is Backstage’s answer to “the wiki is a graveyard.” Each repo carries a docs/ folder of Markdown plus an mkdocs.yml; a MkDocs build turns that into static HTML, and the backstage.io/techdocs-ref: dir:. annotation makes it a Docs tab on the service’s page. Because the docs sit in the same pull request as the code change, they get reviewed with it.
# mkdocs.yml — sits next to catalog-info.yaml in the service repo site_name: 'Checkout Service' nav: - Home: index.md - Runbook: runbook.md - Architecture: architecture.md plugins: - techdocs-core # required: gives TechDocs its theme + extensions
Day-to-day commands
☺ Like you’re 10: Backstage has no fancy control tool — you run it like any web app, with the same commands a website developer uses.
There is no backstage CLI that talks to a running server — it is a Node application, so the “CLI you actually use” is Yarn, npx and its HTTP API.
Creating, running and building the app
# Scaffold a brand-new portal into ./acme-portal (creates a monorepo) npx @backstage/create-app@latest --path acme-portal # Run frontend (:3000) and backend (:7007) together, with hot reload yarn install yarn dev # Run just one half — useful when debugging the backend yarn workspace backend start yarn workspace app start # Add a plugin: install the package, then wire it into packages/app or backend yarn workspace app add @backstage/plugin-kubernetes yarn workspace backend add @backstage/plugin-kubernetes-backend # Scaffold your own plugin inside the monorepo yarn new # Type-check, lint and test the whole workspace before you ship yarn tsc && yarn lint:all && yarn test:all # Production build, then a container image from the backend Dockerfile yarn build:all yarn build:backend --config ../../app-config.yaml docker image build . -f packages/backend/Dockerfile --tag acme/backstage:2024.11.1 # THE upgrade command — bumps every @backstage/* package to a release line npx @backstage/cli versions:bump
TechDocs and the catalog API
# Preview a repo's TechDocs locally, inside the real Backstage shell
npx @techdocs/cli serve
# Build docs in CI and publish them to the bucket (builder: 'external')
npx @techdocs/cli generate --source-dir . --output-dir ./site
npx @techdocs/cli publish \
--publisher-type awsS3 --storage-name acme-techdocs \
--entity default/component/checkout
# Ask the catalog what it knows — everything is a plain REST API
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/catalog/entities/by-query?filter=kind=component,spec.lifecycle=production" \
| jq -r '.items[] | "\(.metadata.name)\t\(.spec.owner)"'
# One entity, by kind/namespace/name
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/catalog/entities/by-name/component/default/checkout" | jq .
# Register a repo by hand (what the "Register existing component" button does)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"type":"url","target":"https://github.com/acme/checkout/blob/main/catalog-info.yaml"}' \
"$BASE/api/catalog/locations"
# List locations, then delete a stale one (this is how you evict dead entities)
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/catalog/locations" | jq .
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE/api/catalog/locations/<id>"The catalog API is for more than debugging — it is how you build scorecards, ownership reports and “every tier-1 service must have a runbook” checks. See the command reference for the wider toolbox.
Gotchas and failure modes
☺ Like you’re 10: The shop window is a real building you have to clean, repair and keep stocked — it doesn’t look after itself.
You are not installing a product — you are adopting an application
This is the gotcha that sinks adoptions. Backstage ships as source you fork and own: a TypeScript monorepo with a frontend, a backend, a Dockerfile and your own plugin wiring. You own its build pipeline, its Postgres, its SSO, its uptime, its dependency CVEs and — the big one — its upgrades. The project releases often and has moved through significant architectural shifts (notably the new backend system), so a portal left untouched for a year is genuinely painful to bring forward. Budget a named owner before you start, run npx @backstage/cli versions:bump on a cadence, and upgrade in small hops rather than one heroic leap.
An out-of-date catalog is worse than none, because people stop trusting it and quietly go back to Slack. Three failure modes recur. Stale entities: a repo is deleted but the Location that registered it still exists, so a ghost service lingers — delete the location, not the entity. Orphans: an entity whose parent location disappeared; Backstage tags it with the backstage.io/orphan annotation so you can search for it. Silent processing errors: a malformed catalog-info.yaml or an unreachable URL surfaces as an error on the entity page and in the backend logs, and the entity simply never refreshes. Prefer providers over manual registration, alert on catalog processing errors, and treat “every production repo has a valid catalog-info.yaml” as a measurable goal.
The scaffolder is a credentialed robot
To create repositories, grant team access and open pull requests, the scaffolder holds real credentials — ideally a scoped GitHub App rather than a personal access token tied to an employee who may leave. Those credentials sit in your backend, which makes it a high-value target: keep templates in a repo only the platform team can merge to, restrict ingested kinds via catalog.rules, and pull the secret in with External Secrets rather than baking it into config. See Secrets Management.
Two defaults bite hard. The generated app uses a guest sign-in provider and an in-memory SQLite database — fine for yarn dev, catastrophic in production, where you need real SSO and Postgres. And Backstage’s permission framework is opt-in: until you write a policy, any authenticated user who reaches the portal can execute any template — which, given the scaffolder’s credentials, means anyone can create repositories in your org. Turn on SSO, enable the permission backend, and gate expensive templates before you announce the portal company-wide. That is a governance requirement, not a nice-to-have.
Performance, TechDocs and the empty-portal trap
Leaving techdocs.builder: 'local' in production means the backend runs a MkDocs build on request — slow, CPU-hungry, and failing whenever a repo’s docs are broken; build docs in CI (builder: 'external') and publish to object storage instead. Discovery providers crawling thousands of repos at a tight schedule.frequency will exhaust your SCM API rate limit; widen the interval and use a GitHub App for its higher quota. Blank Kubernetes or Argo CD tabs are almost always a missing or mismatched annotation rather than a broken plugin — check backstage.io/kubernetes-id against the real pod labels, and see Triage: Delivery and Triage: Workloads for the systems underneath. Finally, the softest failure: a thin portal nobody visits. Seed it with the fifty services people actually search for, put one genuinely useful template on the front page, and measure adoption — a portal is a product, and it dies of indifference far more often than of bugs.
Do the whole loop locally, no cloud bill. Run npx @backstage/create-app@latest then yarn dev — a portal on localhost:3000 in about five minutes. Add a catalog-info.yaml to any repo of your own (a Component plus a Group that owns it) and register it through the UI; watch the ownership edge appear on both pages. Next drop an mkdocs.yml and a docs/index.md into that repo, add backstage.io/techdocs-ref: dir:., and see the Docs tab light up. Finally, copy one of the built-in example templates and point its fetch:template step at a skeleton containing just a README and a catalog-info.yaml. Catalog entry, docs and a golden path in half an hour — and you will know exactly where the seams are.
Alternatives and when to choose it
☺ Like you’re 10: Backstage is the build-it-yourself shop window. Some companies buy one ready-made instead, and some don’t need a window at all.
First ask whether you need a portal yet
With twenty services and one team, a good README and a make new-service script beat a Node monorepo you must nurse. A portal earns its keep when discovery costs real time — many teams, hundreds of components, and golden paths worth advertising. Adopting one to create that demand is how portals become expensive screensavers.
Build, buy, or skip
| Option | Shape | Strengths | Choose it when |
|---|---|---|---|
| Backstage (self-hosted) | Open framework you fork, extend and run | No licence cost, huge plugin ecosystem, unlimited customisation, CNCF-governed | You have engineers to own a TypeScript app and want the portal shaped like your platform |
| Backstage distributions (Red Hat Developer Hub, Spotify Portal, Roadie) | Backstage, packaged and supported | Same APIs and templates; upgrades and hardening are someone else’s job | You want Backstage’s model without owning its lifecycle |
| Closed-source portals (Port, Cortex, OpsLevel, Harness IDP) | SaaS, configuration-driven | Fast to value, opinionated catalogs and scorecards, no app to maintain | Time-to-value beats customisation and a per-seat bill is acceptable |
| Platform orchestrators (Humanitec, Kratix) | Backend workflow engine, not a UI | Executes the golden path rather than displaying it | Your gap is orchestration, not discovery — often sits behind a portal |
CLI + templates (cookiecutter, a repo template) | No portal at all | Near-zero maintenance; developers already live in a terminal | Small org, or users who genuinely prefer a CLI to a web form |
| Git + CODEOWNERS + a wiki | The status quo | Free; already exists | Under ~30 services — but expect ownership rot as you grow |
Two habits de-risk the choice. Keep the source of truth in Git — entity descriptions beside the code, templates in a reviewable repo — so migrating portals means re-pointing an ingester, not re-entering a thousand records. And remember the portal is the thinnest layer in the stack; the value lives in the architecture behind it. Browse the tool landscape to see the neighbours it fronts.
Foxy: Leadership saw a Backstage demo. They want it live by Friday. That’s… a website, right? How hard can it be?
Mira: It’s a TypeScript monorepo we fork, own and upgrade forever. Friday gets you an empty catalog. And an empty catalog is worse than none — people try it once, find nothing, and never come back.
Dot: Honestly? I don’t want a portal. I want one page for the payments service with the runbook, the owner, and whether it’s deployed. If the portal gives me that, I’ll live in it.
Gizmo: Easy! I’ll type all four hundred services into the catalog by hand this weekend. Nice and fast, no tedious discovery config. 🤑
Timmy: And in six weeks half of them are wrong and nobody knows which half. Put catalog-info.yaml next to the code and let a GitHub entity provider crawl the org. The map has to update itself, Gizmo, or it’s a lie with a search box.
Mira: And one template on the front page that actually works. New service → repo, CI, namespace, database claim, dashboards, on-call. That single button is worth more than three hundred catalog entries.
Benny: Just remember the button only writes a pull request. Argo still does the deploying. The portal is the shop window — I’m still the one laying the rails behind it.
Exam relevance and going further
☺ Like you’re 10: Backstage won’t be a hands-on task, but the idea behind it turns up all over the exam — and you can’t look up its website on the day.
Backstage is not on the official CNPE tool list, so do not expect a task that makes you install Node and run yarn dev. What you should expect is the concept, repeatedly: self-service, golden paths, software catalogs and developer portals run through the exam’s self-service and DevEx competencies, and Backstage is the reference implementation everyone means. Know it well enough to describe it precisely and to distinguish it from the things it fronts.
The documentation allowlist — read this twice
During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, task-specific documentation explicitly linked in the exam’s Quick Reference box, and local man pages and /usr/share docs on the exam machine. backstage.io is not on that list, and neither is any plugin’s README. If a scenario mentions a portal, you are answering from understanding, not from a tab. Spend your memorisation budget on the manifests you might actually have to type — Kubernetes objects and the CRDs on Know Cold — and keep Backstage as conceptual knowledge you can explain in a sentence.
⚖ CNPA vs CNPE — That allowlist is a CNPE mechanic: CNPE is hands-on, and permits those narrow lookups mid-task. CNPA is stricter, not looser — a fully closed-book, multiple-choice exam with zero external references of any kind, not even kubernetes.io. Knowing what Backstage is and does is still worth having cold for CNPA's closed-book recall.
What to be able to do cold
Define an internal developer portal and contrast it with an internal developer platform in one sentence each. Name Backstage’s three cores — Software Catalog, Software Templates (scaffolder), TechDocs. Recognise a catalog-info.yaml on sight: apiVersion: backstage.io/v1alpha1, a kind from {Component, API, Resource, System, Domain, Group, User, Location}, and spec.owner/spec.lifecycle/spec.type. Explain that relations like ownedBy and partOf are derived, not written. Say what a template’s parameters versus steps do, and name three actions (fetch:template, publish:github, catalog:register). Explain why the scaffolder writes to Git rather than to a cluster, and why that keeps GitOps intact. And be ready to argue the honest trade-off: a portal you cannot staff is worse than no portal. Reinforce it with Self-Service & Developer Portals, Developer Experience, Team Topologies and the glossary.
Official resources for after the exam
Outside the exam, start at backstage.io/docs — the Software Catalog, Software Templates and TechDocs sections are the three you will re-read most, and the descriptor format reference is the one to bookmark. The source lives at github.com/backstage/backstage; the upgrade helper diffs your version against the latest and is indispensable at upgrade time; plugins are indexed at backstage.io/plugins; the project page is cncf.io/projects/backstage. When you wire it to real infrastructure, pair it with Crossplane for the APIs behind the buttons and Best Practices for keeping it honest.
1. In one sentence each, what is an internal developer platform and an internal developer portal? 2. Name Backstage’s three core capabilities. 3. Where does a catalog-info.yaml live, and why there? 4. You set spec.owner: payments-team but the owner link goes nowhere — what went wrong? 5. In a Software Template, what is the difference between parameters and steps, and what does the skeleton directory contain? 6. Your scaffolder can create repos in your GitHub org — name two controls you should have in place. 7. During the exam, where can you look up the Backstage entity schema?
Check your answers
- The platform is the paved road itself — clusters, GitOps, control planes, pipelines, policy. The portal is the web UI in front of it that makes the road discoverable. Backstage is the portal; a portal without a platform is a menu with no kitchen.
- Software Catalog, Software Templates run by the scaffolder, and TechDocs. Everything else arrives as plugins.
- At the root of the repository it describes, so it ships in the same pull request as the code and stays true. Discovery providers then crawl the org and ingest every such file automatically.
- The referenced
Groupis not in the catalog — a dangling owner reference. Ingest org data (Groups and Users) before components, and prefergroup:default/payments-team. parametersis JSON Schema rendered as the form;stepsis the ordered list of server-side actions run on submit (fetch:template,publish:github,catalog:register…). The skeleton is the project tree the template renders — Dockerfile, CI workflow, manifests,catalog-info.yaml— with${{ values.* }}placeholders.- Any two of: a scoped GitHub App rather than a personal token; the credential in a secret manager via External Secrets; restricted merge rights on the templates repo; SSO plus the permission framework so not every user can run every template;
catalog.ruleslimiting ingested kinds. - You can’t —
backstage.iois not on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and localman//usr/sharedocs only). Backstage is conceptual knowledge; spend memorisation effort on Know Cold.