Skip to content

Module 1.3: Backstage Catalog & Infrastructure

Complexity: [COMPLEX] - Covers two exam domains (44% of CBA combined)

Time to Complete: 60-75 minutes

Prerequisites: Module 1 (Backstage Overview), Module 2 (Plugins & Extensibility)


After completing this module, you will be able to:

  1. Define catalog entities (Components, APIs, Systems, Domains) with proper metadata, relationships, and lifecycle annotations
  2. Configure entity providers and processors that auto-discover services from GitHub, Kubernetes, or LDAP into the catalog
  3. Deploy Backstage to Kubernetes with PostgreSQL persistence, Ingress, and environment-specific configuration
  4. Design a catalog taxonomy that models your organization’s ownership, dependencies, and API contracts

The software catalog is the beating heart of Backstage. Without it, Backstage is just a plugin framework with a pretty UI. With it, you have a single pane of glass over every service, API, team, and piece of infrastructure your organization owns. The CBA exam dedicates 22% to the catalog (Domain 3) and another 22% to infrastructure (Domain 2)—together, that is 44% of your score.

Get these two domains right and you are nearly halfway to passing before you even touch plugins or TechDocs.

The Library Analogy

Think of the Backstage catalog as a library’s card catalog system. Every book (Component) has a card describing it—author, genre, location. Some books reference other books (API relationships). The librarian (entity processor) receives new books, catalogs them, and shelves them. The building itself—shelves, lighting, HVAC—is the infrastructure. You need both: a great catalog system is useless if the building has no electricity, and a beautiful building with empty shelves helps nobody.


By the end of this module, you’ll understand:

  • All nine entity kinds and when to use each one
  • How entities get into the catalog (manual and automated)
  • How to write and structure catalog-info.yaml files
  • The Backstage architecture: frontend, backend, database, proxy
  • How to configure Backstage with app-config.yaml
  • Production deployment considerations
  • Common troubleshooting patterns for catalog issues

  1. Spotify’s catalog tracks over 10,000 components across hundreds of teams. The software catalog was the original reason Backstage was built—everything else came later.
  2. Entity refresh is not instant. By default, Backstage re-processes entities every 100-200 seconds. This trips up almost every new admin who registers something and wonders why it is not appearing.
  3. The Backstage proxy (/api/proxy) lets the frontend call external APIs without exposing credentials to the browser—a pattern so useful that many teams use Backstage as their universal API gateway during development.
  4. You can run Backstage without a single plugin installed. The catalog alone provides enough value that some organizations deploy it purely as a service directory with ownership tracking.

Part 1: The Software Catalog (Domain 3 — 22%)

Section titled “Part 1: The Software Catalog (Domain 3 — 22%)”

Everything in the Backstage catalog is an entity. Each entity has a kind, an apiVersion, metadata, and a spec. There are nine built-in kinds:

┌─────────────────────────────────────────────────────────────────┐
│ BACKSTAGE ENTITY KINDS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ OWNERSHIP ORGANIZATIONAL CATALOG MACHINERY │
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Component │ │ Group │ │ Location │ │
│ │ (service, │ │ (team, │ │ (points │ │
│ │ library) │ │ dept) │ │ to YAML)│ │
│ └───────────┘ └──────────┘ └──────────┘ │
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
│ │ API │ │ User │ │ Template │ │
│ │ (REST, │ │ (person)│ │ (scaffol-│ │
│ │ gRPC) │ └──────────┘ │ ding) │ │
│ └───────────┘ └──────────┘ │
│ ┌───────────┐ GROUPING │
│ │ Resource │ ┌──────────┐ │
│ │ (DB, S3, │ │ System │ │
│ │ queue) │ │ (group │ │
│ └───────────┘ │ of comp)│ │
│ └──────────┘ │
│ ┌──────────┐ │
│ │ Domain │ │
│ │ (business│ │
│ │ area) │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
KindPurposeExample
ComponentA piece of software (service, website, library)payments-service, react-ui-library
APIA boundary between components (REST, gRPC, GraphQL, AsyncAPI)payments-api (OpenAPI spec)
ResourcePhysical or virtual infrastructure a component depends onorders-db (PostgreSQL), events-queue (Kafka topic)
SystemA collection of components and APIs that form a productpayments-system (groups payments service + API + DB)
DomainA business area grouping related systemsfinance (groups payments, billing, invoicing systems)
GroupA team or organizational unitplatform-team, backend-guild
UserAn individual personjane.doe
LocationA pointer to other entity definition filesA URL referencing a catalog-info.yaml in a repo
TemplateA software template for scaffolding new projectsspringboot-service-template

Key relationships between entity kinds:

Domain
└── System
├── Component ──ownedBy──► Group/User
│ ├── providesApi ──► API
│ ├── consumesApi ──► API
│ └── dependsOn ──► Resource
└── API ──ownedBy──► Group/User

Every entity is described by a YAML descriptor. The standard name is catalog-info.yaml and it typically lives at the root of a repository:

catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-service
description: Handles all payment processing
annotations:
github.com/project-slug: myorg/payments-service
backstage.io/techdocs-ref: dir:.
tags:
- java
- payments
links:
- url: https://payments.internal.myorg.com
title: Production
icon: dashboard
spec:
type: service
lifecycle: production
owner: team-payments
system: payments-system
providesApis:
- payments-api
dependsOn:
- resource:payments-db

Required fields for every entity:

  • apiVersion — always backstage.io/v1alpha1 for built-in kinds
  • kind — one of the nine kinds above
  • metadata.name — unique within its kind+namespace; lowercase, hyphens, max 63 chars
  • spec — varies by kind

Annotations are the glue between catalog entities and Backstage plugins. They tell plugins where to find data about a component. This is a critical exam topic.

AnnotationWhat It Does
github.com/project-slugLinks entity to a GitHub repo (org/repo)
backstage.io/techdocs-refTells TechDocs where to find docs (dir:. = same repo)
backstage.io/source-locationSource code URL for the entity
jenkins.io/job-full-nameLinks to a Jenkins job
pagerduty.com/service-idLinks to PagerDuty for on-call info
backstage.io/managed-by-locationWhich Location entity registered this entity
backstage.io/managed-by-origin-locationOriginal Location that first introduced the entity

Annotations are how Backstage stays loosely coupled. The core catalog does not know about Jenkins or PagerDuty. Plugins read annotations to find the data they need.

1.4 Manual Registration: Location Entities

Section titled “1.4 Manual Registration: Location Entities”

The simplest way to get entities into the catalog is manual registration using Location entities.

Option A: Register via the UI

Click “Register Existing Component” in Backstage, paste a URL to a catalog-info.yaml, and Backstage creates a Location entity pointing to that URL.

Option B: Static Location in app-config.yaml

app-config.yaml
catalog:
locations:
- type: url
target: https://github.com/myorg/payments-service/blob/main/catalog-info.yaml
rules:
- allow: [Component, API]
- type: file
target: ../../examples/all-components.yaml
rules:
- allow: [Component, System, Domain]

Location entity YAML (can also be its own file):

apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: myorg-payments
description: Payments team components
spec:
type: url
targets:
- https://github.com/myorg/payments-service/blob/main/catalog-info.yaml
- https://github.com/myorg/payments-api/blob/main/catalog-info.yaml

Exam tip: A Location entity can reference multiple targets. This is useful for registering many repos at once without automated discovery.

Manual registration does not scale. For organizations with hundreds or thousands of repos, Backstage supports discovery providers that automatically find and register entities.

GitHub Discovery:

app-config.yaml
catalog:
providers:
githubDiscovery:
myOrgProvider:
organization: 'myorg'
catalogPath: '/catalog-info.yaml' # where to look in each repo
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }

This scans every repo in the myorg GitHub organization, checks if /catalog-info.yaml exists, and automatically registers any entities found.

GitLab Discovery:

catalog:
providers:
gitlab:
myGitLab:
host: gitlab.mycompany.com
branch: main
fallbackBranch: master
catalogFile: catalog-info.yaml
group: 'mygroup' # optional: limit to a group
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }

GitHub Org Data Provider (for users and teams):

catalog:
providers:
githubOrg:
myOrgProvider:
id: production
orgUrl: https://github.com/myorg
schedule:
frequency: { hours: 1 }
timeout: { minutes: 10 }

This imports GitHub teams as Group entities and GitHub org members as User entities automatically—no need to maintain user YAML files by hand.

1.6 Entity Processors and Custom Providers

Section titled “1.6 Entity Processors and Custom Providers”

The catalog has a processing pipeline that runs continuously:

┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Ingestion │────►│ Processing │────►│ Stitching │
│ │ │ │ │ │
│ - Locations │ │ - Validate YAML │ │ - Resolve refs │
│ - Discovery │ │ - Run processors│ │ - Build relation │
│ - Providers │ │ - Emit entities │ │ graph │
│ │ │ - Emit errors │ │ - Final entity │
└──────────────┘ └─────────────────┘ └──────────────────┘
│ │ │
▼ ▼ ▼
Entity enters Entity validated Entity visible
the pipeline and enriched in the catalog

Entity processors are functions that run on every entity during the processing phase. Built-in processors handle things like:

  • Validating entity schema
  • Resolving Location targets into child entities
  • Extracting relationships from spec fields

Custom entity providers let you ingest entities from any source—a CMDB, a spreadsheet, an internal API. They implement the EntityProvider interface:

import { EntityProvider, EntityProviderConnection } from '@backstage/plugin-catalog-node';
class MyCustomProvider implements EntityProvider {
getProviderName(): string {
return 'my-custom-provider';
}
async connect(connection: EntityProviderConnection): Promise<void> {
// Fetch entities from your custom source
const entities = await fetchFromMySource();
await connection.applyMutation({
type: 'full',
entities: entities.map(entity => ({
entity,
locationKey: 'my-custom-provider',
})),
});
}
}

Entity not appearing after registration:

SymptomLikely CauseFix
Entity never shows upInvalid YAML or schema violationCheck the catalog import page for errors
Entity appears then disappearsrules in app-config block the entity kindAdd the kind to rules: allow
Stale data after repo updateRefresh cycle has not run yetManually refresh via catalog API or wait ~100-200s
Entity shows as orphanedThe Location that registered it was deletedRe-register or remove the orphan
Relationships brokenReferenced entity name does not matchCheck exact name fields; they are case-sensitive

Orphaned entities occur when the Location that originally registered an entity is removed, but the entity itself remains. Backstage marks these as orphans. You can clean them up in the catalog UI or via the API:

Terminal window
# List orphaned entities via the Backstage catalog API
curl http://localhost:7007/api/catalog/entities?filter=metadata.annotations.backstage.io/orphan=true
# Delete a specific orphaned entity
curl -X DELETE http://localhost:7007/api/catalog/entities/by-uid/<entity-uid>

Forcing a refresh:

Terminal window
# Refresh a specific entity
curl -X POST http://localhost:7007/api/catalog/refresh \
-H 'Content-Type: application/json' \
-d '{"entityRef": "component:default/payments-service"}'

Part 2: Backstage Infrastructure (Domain 2 — 22%)

Section titled “Part 2: Backstage Infrastructure (Domain 2 — 22%)”

Backstage is a Node.js application with a clear client-server split:

┌─────────────────────────────────────────────────────────────────┐
│ BROWSER (Client) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Backstage Frontend App (React SPA) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ │
│ │ │ Catalog │ │ TechDocs │ │ Scaffolder│ │ Search │ │ │
│ │ │ Plugin │ │ Plugin │ │ Plugin │ │ Plugin │ │ │
│ │ │ (front) │ │ (front) │ │ (front) │ │ (front) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│ HTTP/REST API calls
┌─────────────────────────────────────────────────────────────────┐
│ BACKSTAGE BACKEND (Node.js) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Catalog │ │ TechDocs │ │ Scaffolder│ │ Auth / Proxy / │ │
│ │ Backend │ │ Backend │ │ Backend │ │ Search Backend │ │
│ └─────┬────┘ └──────────┘ └──────────┘ └───────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────────────────────────────────────┐ │
│ │ Database │ │ Integrations │ │
│ │(Postgres)│ │ GitHub, GitLab, Azure DevOps, LDAP ... │ │
│ └──────────┘ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Key architectural points for the exam:

  1. Frontend — A React single-page application (SPA). Built at compile time. Served as static files. All frontend plugins are compiled into one bundle.
  2. Backend — A Node.js (Express) server. Each backend plugin registers its own API routes under /api/<plugin-id>/. Runs entity processing, scaffolding, TechDocs generation, etc.
  3. Database — SQLite for development, PostgreSQL for production. Stores catalog entities, search index, scaffolder task history.
  4. Integrations — Configured connections to external systems (SCM, CI/CD, cloud providers). Defined in app-config.yaml under the integrations key.

The app-config.yaml file is the central configuration for a Backstage instance. Understanding its structure is essential.

# app-config.yaml — Top-level structure
app:
title: My Company Backstage
baseUrl: http://localhost:3000 # Frontend URL
backend:
baseUrl: http://localhost:7007 # Backend URL
listen:
port: 7007
database:
client: better-sqlite3 # dev default
connection: ':memory:'
cors:
origin: http://localhost:3000
organization:
name: MyOrg
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN} # environment variable substitution
auth:
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
proxy:
endpoints:
'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization: Token token=${PAGERDUTY_TOKEN}
catalog:
locations: []
providers: {}
rules:
- allow: [Component, System, API, Resource, Location, Domain, Group, User, Template]

Environment variable substitution:

Backstage supports ${VAR_NAME} syntax in app-config.yaml. At startup, the backend resolves these from the process environment. This is the primary way to inject secrets.

Configuration layering (config includes):

Terminal window
# You can pass multiple config files — later files override earlier ones
node packages/backend --config app-config.yaml --config app-config.production.yaml

A common pattern:

  • app-config.yaml — base/development configuration
  • app-config.production.yaml — production overrides (database, URLs, auth)
  • app-config.local.yaml — personal local overrides (gitignored)

The proxy plugin (/api/proxy) lets the backend forward requests to external APIs on behalf of the frontend. This solves two problems: CORS restrictions and secret management.

app-config.yaml
proxy:
endpoints:
'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization: Token token=${PAGERDUTY_TOKEN}
'/grafana':
target: https://grafana.internal.myorg.com
headers:
Authorization: Bearer ${GRAFANA_TOKEN}
allowedHeaders: ['Content-Type']

How it works:

Browser Backstage Backend External API
│ │ │
│ GET /api/proxy/pagerduty/ │ │
│ services/PXXXXXX │ │
│─────────────────────────────►│ │
│ │ GET /services/PXXXXXX │
│ │ Authorization: Token ... │
│ │────────────────────────────►│
│ │ │
│ │◄────────────────────────────│
│◄─────────────────────────────│ (response forwarded) │
│ │ │

The browser never sees the PagerDuty API token. It only talks to the Backstage backend. The backend injects the credentials and forwards the request.

Moving from yarn dev to production requires several changes:

Database — switch to PostgreSQL:

app-config.production.yaml
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}

HTTPS and base URLs:

app:
baseUrl: https://backstage.mycompany.com
backend:
baseUrl: https://backstage.mycompany.com
cors:
origin: https://backstage.mycompany.com

In practice, most teams terminate TLS at a load balancer or ingress controller in front of Backstage, not in the Node.js process itself.

Authentication:

Backstage supports multiple auth providers (GitHub, Google, Okta, SAML, etc.). In production, authentication is not optional. Without it, anyone on the network can access the catalog.

auth:
environment: production
providers:
github:
production:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

Scaling considerations:

  • Backstage is a stateless Node.js app (state is in the database). You can run multiple replicas behind a load balancer.
  • The catalog processing loop should ideally run on a single instance to avoid duplicate work. Use the @backstage/plugin-catalog-backend leader election or dedicate one replica for processing.
  • Search indexing is also best run on a single replica to avoid index conflicts.
  • Static frontend assets can be served via CDN for better performance.

For the exam, understand the request flow:

1. User opens browser → loads React SPA from backend (static files)
2. SPA boots → calls backend APIs: /api/catalog, /api/techdocs, etc.
3. Backend plugins handle API calls → query database, call integrations
4. Backend returns JSON → SPA renders UI
5. For external data → SPA calls /api/proxy/* → backend forwards to external APIs

Port defaults:

  • Frontend dev server: 3000 (development only — in production, served by backend)
  • Backend: 7007

In production, there is typically a single serving endpoint. The backend serves both the static frontend bundle and its own API routes. A reverse proxy or Kubernetes Ingress sits in front.


War Story: The Case of the 10,000 Orphaned Entities

Section titled “War Story: The Case of the 10,000 Orphaned Entities”

A platform team at a mid-size fintech company set up GitHub discovery to auto-register every repo in their organization. Within a week, the catalog had 10,000 entities—but morale was not what they expected. Developers were complaining that search was useless. The catalog was full of archived repos, forks, test projects, and abandoned experiments.

Worse, when they tried to clean up by deleting the discovery provider config, the entities did not disappear. They became orphans—still visible in the catalog but no longer refreshed. The team spent two days writing scripts to bulk-delete orphans via the catalog API.

Lessons learned:

  1. Always scope discovery providers with filters (topic tags, path patterns, team ownership).
  2. Understand the orphan lifecycle before removing discovery providers.
  3. Start with manual registration for your most important services, then gradually expand automated discovery.
  4. Use catalog.rules to restrict which entity kinds can be registered from which sources.

MistakeWhy It HappensWhat To Do Instead
Using SQLite in productionIt is the default and “works” in devAlways configure PostgreSQL for production
Not scoping discovery providersGitHub discovery imports every repoUse topic filters, path patterns, or allowlists
Expecting instant catalog updatesDevelopers register YAML and refresh the page immediatelyExplain the ~100-200s refresh cycle; use manual refresh API for urgent updates
Hardcoding secrets in app-config.yamlCopy-pasting tokens during setupUse ${ENV_VAR} substitution; never commit secrets
Forgetting rules: allow for entity kindsRegister a Template but it never appearsEach Location source needs explicit rules for allowed kinds
Running TLS termination in Node.jsSeems simpler than a reverse proxyUse an ingress controller or load balancer for TLS; Node.js TLS is not needed
Not configuring auth for productionDev mode works without itEvery production instance must have authentication enabled
Ignoring orphaned entitiesThey accumulate silentlyMonitor orphan count; establish a cleanup process

Test your understanding of Backstage catalog and infrastructure.

Q1: Which entity kind represents a boundary between components?

Answer

API. The API kind represents a contract/boundary between components. A Component providesApi and another Component consumesApi. API entities can describe REST (OpenAPI), gRPC (protobuf), GraphQL, or AsyncAPI interfaces.

Q2: What is the default refresh interval for catalog entity processing?

Answer

Approximately 100-200 seconds. The catalog processing loop continuously cycles through entities, but there is no guarantee of instant updates. You can trigger a manual refresh via POST /api/catalog/refresh with the entityRef.

Q3: How do you inject secrets into app-config.yaml?

Answer

Use environment variable substitution with ${VARIABLE_NAME} syntax. For example: token: ${GITHUB_TOKEN}. Backstage resolves these at startup from the process environment. Never hardcode secrets in config files.

Q4: What is the purpose of the Backstage proxy plugin?

Answer

The proxy plugin (/api/proxy) forwards requests from the frontend through the backend to external APIs. This solves CORS issues and keeps API credentials server-side. The browser never sees the external service tokens—only the backend injects them before forwarding.

Q5: Name two ways entities can be registered in the catalog.

Answer
  1. Manual registration — via the UI (“Register Existing Component” button) or by adding static Location entries in app-config.yaml under catalog.locations.
  2. Automated discovery — using providers like githubDiscovery, gitlab, or githubOrg configured under catalog.providers in app-config.yaml.

Other valid answers include: custom entity providers (programmatic) or direct API calls.

Q6: What database should be used for a production Backstage deployment?

Answer

PostgreSQL. SQLite (or better-sqlite3) is only suitable for local development. PostgreSQL supports concurrent connections, is durable, and handles the catalog processing workload in production. Configure it via backend.database.client: pg in app-config.production.yaml.

Q7: What happens to entities when their source Location is deleted?

Answer

They become orphaned entities. They remain in the catalog but are no longer refreshed from their source. Orphans are flagged with the annotation backstage.io/orphan: 'true'. They should be cleaned up either through the UI or via the catalog API (DELETE /api/catalog/entities/by-uid/<uid>).

Q8: How does configuration layering work in Backstage?

Answer

You pass multiple --config flags when starting the backend: node packages/backend --config app-config.yaml --config app-config.production.yaml. Later files override values from earlier files (deep merge). Common pattern: base config, production overrides, and a gitignored local config for personal development settings.

Q9: Which annotation links a Backstage entity to its GitHub repository?

Answer

github.com/project-slug with the value org/repo-name. For example: github.com/project-slug: myorg/payments-service. This annotation is read by GitHub-related plugins to display pull requests, CI status, code owners, and other repo-level information.

Q10: In a production Kubernetes deployment of Backstage, why should catalog processing run on a single replica?

Answer

To avoid duplicate processing work and potential conflicts. If multiple replicas all run the processing loop simultaneously, they may redundantly fetch the same sources, create duplicate refresh cycles, and potentially conflict on database writes. The @backstage/plugin-catalog-backend supports leader election to ensure only one replica performs catalog processing while others handle API requests.


Hands-On Exercise: Build a Multi-Entity Catalog

Section titled “Hands-On Exercise: Build a Multi-Entity Catalog”

Objective: Create a complete catalog structure with multiple entity kinds, register them, and verify the relationships.

What you’ll need: A running Backstage instance (npx @backstage/create-app@latest if you do not have one).

Create a file called catalog-entities.yaml in your Backstage project root:

---
apiVersion: backstage.io/v1alpha1
kind: Domain
metadata:
name: commerce
description: All commerce-related systems
spec:
owner: group:platform-team
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: orders-system
description: Handles order lifecycle
spec:
owner: group:backend-team
domain: commerce
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: orders-service
description: REST API for order management
annotations:
backstage.io/techdocs-ref: dir:.
tags:
- java
- springboot
spec:
type: service
lifecycle: production
owner: group:backend-team
system: orders-system
providesApis:
- orders-api
dependsOn:
- resource:orders-db
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: orders-api
description: Orders REST API
spec:
type: openapi
lifecycle: production
owner: group:backend-team
system: orders-system
definition: |
openapi: "3.0.0"
info:
title: Orders API
version: 1.0.0
paths:
/orders:
get:
summary: List orders
responses:
'200':
description: OK
---
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: orders-db
description: PostgreSQL database for orders
spec:
type: database
owner: group:backend-team
system: orders-system
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: backend-team
description: Backend engineering team
spec:
type: team
children: []
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: platform-team
description: Platform engineering team
spec:
type: team
children: []

Add to your app-config.yaml:

catalog:
rules:
- allow: [Component, System, API, Resource, Location, Domain, Group, User, Template]
locations:
- type: file
target: ./catalog-entities.yaml
rules:
- allow: [Domain, System, Component, API, Resource, Group]
Terminal window
# Start Backstage in development mode
yarn dev

Open http://localhost:3000 and verify:

  1. Navigate to the Catalog — you should see orders-service listed as a Component
  2. Click on orders-service — verify the System is orders-system
  3. Check the API tab — orders-api should appear under “Provided APIs”
  4. Check the Dependencies tab — orders-db should appear
  5. Navigate to orders-system — verify it groups the component, API, and resource
  6. Navigate to the commerce Domain — verify it contains orders-system

Add a proxy endpoint to app-config.yaml:

proxy:
endpoints:
'/jsonplaceholder':
target: https://jsonplaceholder.typicode.com

Restart the backend and test:

Terminal window
# This request goes through the Backstage proxy
curl http://localhost:7007/api/proxy/jsonplaceholder/todos/1

You should get a JSON response from jsonplaceholder.typicode.com, forwarded through your Backstage backend.

  • All seven entities appear in the catalog
  • orders-service shows correct owner (backend-team), system, API, and dependency
  • Domain > System > Component hierarchy is visible in the UI
  • You understand the refresh cycle (modify an entity, observe the delay before the catalog updates)
  • Proxy endpoint returns data from the external API (optional)

TopicRemember This
Entity kinds9 built-in: Component, API, Resource, System, Domain, Group, User, Location, Template
catalog-info.yamlLives in repo root; apiVersion, kind, metadata, spec are required
AnnotationsConnect entities to plugins; key discovery mechanism
RegistrationManual (UI or static locations) vs. automated (discovery providers)
ProcessingContinuous loop with ~100-200s cycle; ingestion → processing → stitching
ArchitectureReact SPA frontend + Node.js backend + PostgreSQL database
app-config.yamlLayered config; ${ENV_VAR} for secrets; --config flag for overrides
Proxy/api/proxy/* forwards frontend requests through backend to external APIs
ProductionPostgreSQL, HTTPS (via ingress), authentication required, single processing replica

CBA Track Overview — Domain 4: Templates, documentation-as-code, and the golden path for new services.