Tools · Backstage

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?”

☺ Explain it like I’m 10

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.

🦋🦆Your hosts for this topic: Mira the Butterfly & Dot the Duck — Mira builds the storefront, wires the catalogue and writes the golden-path templates so a whole service arrives from one form; Dot is the developer standing at the window, and she’ll tell you honestly whether the buttons actually help her ship or just look pretty.

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.

◆ Key idea — a portal is not a platform

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.

🦆 Dot’s-eye view

“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.

🦆 Developer one URL, one login Backstage app — you build, deploy and upgrade this Frontend — React plugin UIs catalog pages · template forms · entity tabs Catalog ingest · graph Scaffolder templates TechDocs docs-as-code Auth & Permissions SSO · policy PostgreSQL cache of entities · scaffolder task state — not the source of truth rebuildable: wipe it and the catalog re-ingests from Git Backstage never deploys — it writes to Git and reads from everything else Outside the portal Git repos catalog-info.yaml · docs/ SCM API create repo · open PR Object store built TechDocs HTML Read-only windows Kubernetes · Argo CD Prometheus · PagerDuty

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.

KindWhat it representsKey spec fields
ComponentA piece of software you build and run — a service, website, librarytype, lifecycle, owner, system, providesApis, consumesApis, dependsOn
APIThe interface a component exposes — the contract, not the codetype (openapi, asyncapi, graphql, grpc), lifecycle, owner, definition
ResourceInfrastructure a component needs — database, bucket, queue, clustertype, owner, system, dependsOn
SystemA bundle of components, APIs and resources that work as one productowner, domain
DomainA business area grouping related systems (“payments”, “search”)owner
GroupA team or org unit — the thing that owns other entitiestype, profile, parent, children, members
UserA person, usually ingested from your identity providerprofile, memberOf
LocationA pointer to other catalog files — how ingestion fans outtype, target / targets
TemplateA 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.

⚠ Owner must be a real entity

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.

◆ Key idea — the template is the golden path made executable

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.

⚠ The catalog is only as good as its ingestion

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.

⚠ Auth and permissions are opt-in, and the defaults are not production

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.

🦋 Mira’s workshop · 30 min

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

OptionShapeStrengthsChoose it when
Backstage (self-hosted)Open framework you fork, extend and runNo licence cost, huge plugin ecosystem, unlimited customisation, CNCF-governedYou 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 supportedSame APIs and templates; upgrades and hardening are someone else’s jobYou want Backstage’s model without owning its lifecycle
Closed-source portals (Port, Cortex, OpsLevel, Harness IDP)SaaS, configuration-drivenFast to value, opinionated catalogs and scorecards, no app to maintainTime-to-value beats customisation and a per-seat bill is acceptable
Platform orchestrators (Humanitec, Kratix)Backend workflow engine, not a UIExecutes the golden path rather than displaying itYour gap is orchestration, not discovery — often sits behind a portal
CLI + templates (cookiecutter, a repo template)No portal at allNear-zero maintenance; developers already live in a terminalSmall org, or users who genuinely prefer a CLI to a web form
Git + CODEOWNERS + a wikiThe status quoFree; already existsUnder ~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.

🎬 At the Platform Guild
🦊

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

⚠ Backstage’s own docs are not available during the exam

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.

🐢 Timmy’s checkpoint

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
  1. 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.
  2. Software Catalog, Software Templates run by the scaffolder, and TechDocs. Everything else arrives as plugins.
  3. 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.
  4. The referenced Group is not in the catalog — a dangling owner reference. Ingest org data (Groups and Users) before components, and prefer group:default/payments-team.
  5. parameters is JSON Schema rendered as the form; steps is 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.
  6. 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.rules limiting ingested kinds.
  7. You can’t — backstage.io is not on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/share docs only). Backstage is conceptual knowledge; spend memorisation effort on Know Cold.