Skip to content

Backstage & Developer Portals - Primer

Why This Matters

Who made it: Backstage was created at Spotify and open-sourced in March 2020. It became a CNCF Incubating project in 2022. Spotify built it internally to manage over 2,000 microservices across hundreds of engineering teams. The name "Backstage" reflects the idea of the infrastructure and tooling that operates behind the scenes of a production.

Backstage is Spotify's open-source platform for building developer portals. It gives engineering organizations a single pane of glass for discovering services, creating new projects, and reading documentation — replacing the scattered wikis, spreadsheets, and tribal knowledge that teams typically rely on. For DevOps, Backstage is the reference implementation of platform engineering: it encodes golden paths (opinionated, well-supported workflows) so teams can ship safely without deep infrastructure expertise. Without it, developers waste hours finding who owns a service, how to create a new one correctly, or where the documentation lives. Backstage centralizes all of this and makes it self-service.

Core Concepts

1. Software Catalog

The catalog is a centralized registry of every service, library, API, and resource in your organization. Each entity is defined by a catalog-info.yaml file checked into its repo, making ownership and dependency information version-controlled and discoverable.

Minimal catalog-info.yaml:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: order-service
  description: Handles order creation, payment, and fulfillment
  annotations:
    github.com/project-slug: myorg/order-service
    backstage.io/techdocs-ref: dir:.
  tags:
    - python
    - fastapi
  links:
    - url: https://grafana.example.com/d/order-service
      title: Grafana Dashboard
      icon: dashboard
spec:
  type: service
  lifecycle: production
  owner: group:team-platform
  system: ecommerce
  providesApis:
    - order-api
  consumesApis:
    - payment-api
    - inventory-api
  dependsOn:
    - resource:order-database

Entity kinds: | Kind | Purpose | |------|---------| | Component | A software component (service, library, website) | | API | An API definition (OpenAPI, gRPC, GraphQL, AsyncAPI) | | Resource | Infrastructure (database, S3 bucket, queue) | | System | A collection of related components | | Domain | A business domain grouping systems | | Group | A team or organizational unit | | User | An individual person | | Template | A software template for the scaffolder | | Location | A pointer to other catalog files to ingest |

Register entities in Backstage:

# catalog-locations.yaml (registered in app-config.yaml)
apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
  name: all-services
spec:
  type: url
  targets:
    - https://github.com/myorg/order-service/blob/main/catalog-info.yaml
    - https://github.com/myorg/payment-service/blob/main/catalog-info.yaml

Auto-discovery (scan entire GitHub org):

# app-config.yaml
catalog:
  providers:
    github:
      myOrg:
        organization: 'myorg'
        catalogPath: '/catalog-info.yaml'
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

2. Software Templates (Scaffolder)

Templates let developers create new services with CI/CD, monitoring, and catalog registration baked in from day one.

# templates/python-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: python-fastapi-service
  title: Python FastAPI Service
  description: Creates a new FastAPI service with CI/CD, Docker, and monitoring
  tags:
    - python
    - fastapi
    - recommended
spec:
  owner: group:team-platform
  type: service

  parameters:
    - title: Service Details
      required:
        - name
        - owner
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z0-9-]+$'
          description: Lowercase with hyphens only
        owner:
          title: Owner Team
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
        description:
          title: Description
          type: string

    - title: Infrastructure
      properties:
        database:
          title: Database
          type: string
          enum: [none, postgresql, redis]
          default: none
        port:
          title: Port
          type: number
          default: 8000

  steps:
    - id: fetch
      name: Fetch Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          owner: ${{ parameters.owner }}
          description: ${{ parameters.description }}
          database: ${{ parameters.database }}
          port: ${{ parameters.port }}

    - id: publish
      name: Create Repository
      action: publish:github
      input:
        repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}
        repoVisibility: internal
        defaultBranch: main

    - id: register
      name: Register in Catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: '/catalog-info.yaml'

  output:
    links:
      - title: Repository
        url: ${{ steps.publish.output.remoteUrl }}
      - title: Open in Catalog
        icon: catalog
        entityRef: ${{ steps.register.output.entityRef }}

Analogy: Think of the Software Catalog as the "CMDB for the microservices era." Traditional CMDBs (Configuration Management Databases) tried to centrally track infrastructure but went stale because humans had to update them. Backstage catalog-info.yaml files live in the repo, so they are updated by the same developer workflow that changes the code.

3. TechDocs (Docs-as-Code)

TechDocs renders Markdown documentation (via MkDocs) directly inside the portal, co-located with the service it describes.

Setup in a service repo:

# mkdocs.yml (in service repo root)
site_name: Order Service
plugins:
  - techdocs-core

nav:
  - Home: index.md
  - Architecture: architecture.md
  - Runbooks: runbooks.md
  - API Reference: api.md

# catalog-info.yaml annotation
metadata:
  annotations:
    backstage.io/techdocs-ref: dir:.
    # Tells Backstage to look for mkdocs.yml in the repo root
# docs/index.md (written by the service team)
# Order Service

## Overview
The order service handles order creation, payment processing, and fulfillment tracking.

## Architecture
...

## Runbooks
- [High latency](runbooks.md#high-latency)
- [Database connection errors](runbooks.md#db-errors)

TechDocs builder configuration (app-config.yaml):

techdocs:
  builder: 'local'        # or 'external' for CI-built docs
  generator:
    runIn: 'local'         # or 'docker'
  publisher:
    type: 'local'          # or 'awsS3', 'googleGcs', 'azureBlobStorage'
    # For production:
    # type: 'awsS3'
    # awsS3:
    #   bucketName: 'my-techdocs-bucket'

4. Plugins

Backstage is extensible through plugins. The plugin ecosystem connects external tools into one developer experience.

Common plugins: | Plugin | Purpose | |--------|---------| | @backstage/plugin-kubernetes | View pod status, logs, events per service | | @backstage/plugin-github-actions | View CI/CD workflow status | | @pagerduty/backstage-plugin | On-call status, incidents per service | | @backstage/plugin-cost-insights | Cloud cost per service/team | | @backstage/plugin-scaffolder | Software templates (built-in) | | @backstage/plugin-techdocs | Documentation (built-in) | | @backstage/plugin-search | Search across catalog, docs, code |

Installing a plugin:

# From the Backstage app directory
cd packages/app
yarn add @backstage/plugin-kubernetes

# Add to app routing (packages/app/src/App.tsx)
# import { KubernetesPage } from '@backstage/plugin-kubernetes';
# <Route path="/kubernetes" element={<KubernetesPage />} />

5. Backstage Setup and Configuration

# Create a new Backstage app
npx @backstage/create-app@latest
# Follow prompts, creates a full Backstage project

# Project structure
my-backstage/
  app-config.yaml          # main configuration
  app-config.local.yaml    # local overrides (gitignored)
  packages/
    app/                   # frontend (React)
    backend/               # backend (Node.js)
  plugins/                 # custom plugins

# Run in development mode
cd my-backstage
yarn dev                   # starts frontend + backend

# Build for production
yarn build
yarn build:backend

# Docker
yarn build-image
# Creates a Docker image for the backend

Core app-config.yaml:

app:
  title: My Company Developer Portal
  baseUrl: http://localhost:3000

backend:
  baseUrl: http://localhost:7007
  listen:
    port: 7007
  database:
    client: pg
    connection:
      host: localhost
      port: 5432
      user: backstage
      password: ${POSTGRES_PASSWORD}

auth:
  providers:
    github:
      development:
        clientId: ${GITHUB_CLIENT_ID}
        clientSecret: ${GITHUB_CLIENT_SECRET}

integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}

catalog:
  rules:
    - allow: [Component, System, API, Resource, Location, Template, Group, User, Domain]
  locations:
    - type: file
      target: ../../catalog-info.yaml
    - type: url
      target: https://github.com/myorg/service-catalog/blob/main/all.yaml

6. Operational Patterns

Catalog ownership enforcement: Every service must have an owner in catalog-info.yaml. This enables on-call routing, cost attribution, and security responsibility.

Gotcha: Backstage's default SQLite database is fine for local development but does not scale. In production, switch to PostgreSQL early. Migrating data from SQLite to PostgreSQL after you have hundreds of catalog entities registered is painful and underdocumented.

Interview tip: If asked about "platform engineering" in an interview, Backstage is the reference implementation to cite. The key insight: a developer portal reduces cognitive load by providing golden paths -- opinionated, pre-configured templates -- rather than forcing every team to figure out CI, monitoring, and deployment from scratch.

Golden paths via templates: Templates encode organizational standards. Instead of a wiki page saying "when creating a new service, remember to set up CI, monitoring, and logging," the template does it automatically.

Service maturity scoring: Use the Backstage Scorecards plugin or custom annotations to track service maturity:

metadata:
  annotations:
    myorg/maturity-level: "3"     # 1-5 scale
    myorg/has-runbooks: "true"
    myorg/has-slo: "true"
    myorg/has-oncall: "true"

Search integration:

# app-config.yaml
search:
  collators:
    techdocs:
      schedule:
        frequency: { minutes: 10 }
    catalog:
      schedule:
        frequency: { minutes: 10 }

Quick Reference

# Create new Backstage app
npx @backstage/create-app@latest

# Development
yarn dev                              # start dev server (frontend + backend)
yarn start                            # frontend only
yarn start-backend                    # backend only

# Build
yarn build && yarn build:backend      # production build
yarn build-image                      # Docker image

# Key files
# app-config.yaml — main configuration
# catalog-info.yaml — entity definition (per repo)
# mkdocs.yml — TechDocs configuration (per repo)
# template.yaml — scaffolder template definition

# Key URLs (default)
# http://localhost:3000 — frontend
# http://localhost:7007 — backend API
# http://localhost:7007/api/catalog/entities — catalog API

Wiki Navigation

  • Platform Engineering Flashcards (CLI) (flashcard_deck, L1) — Platform Engineering
  • Platform Engineering Patterns (Topic Pack, L2) — Platform Engineering