Back to Modern DevOps & Platform Engineering Series

Backstage — Complete Tool Reference Guide

May 14, 2026 Wasil Zafar 30 min read

A comprehensive reference to Backstage — the open-source developer portal platform built by Spotify, now a CNCF incubating project powering internal developer platforms at scale.

Table of Contents

  1. Overview & History
  2. Architecture
  3. Installation & Setup
  4. Software Catalog
  5. TechDocs (Docs as Code)
  6. Software Templates (Golden Paths)
  7. Plugins Ecosystem
  8. Authentication & Authorization
  9. GitHub Integration
  10. Production Deployment
  11. Troubleshooting
  12. Related Deep Dives

Overview & History

Backstage is an open-source platform for building developer portals — a single, unified interface that consolidates infrastructure tooling, services catalog, documentation, and developer workflows into one experience. Originally created at Spotify to manage their rapidly growing microservices ecosystem (2,000+ services, 500+ engineers), Backstage was open-sourced in March 2020 and donated to the Cloud Native Computing Foundation (CNCF) where it reached incubating status in March 2022.

Core Philosophy: Backstage follows the principle of "centralize, don't standardize" — it provides a single pane of glass for developers without forcing rigid tool choices across the organization. Teams keep their preferred tools while gaining unified discoverability.

The platform solves a fundamental problem in modern engineering organizations: as companies scale, developers spend increasing time navigating tooling complexity rather than writing code. Studies from Spotify's internal research showed engineers spending up to 30% of their time on infrastructure tasks — finding documentation, understanding service ownership, provisioning resources, and navigating CI/CD pipelines.

Key Capabilities

  • Software Catalog — Track ownership and metadata for all services, libraries, websites, and ML models
  • Software Templates — Golden paths for creating new projects with best practices baked in
  • TechDocs — Documentation-as-code with MkDocs integration, rendered directly in the portal
  • Plugin Architecture — 200+ community plugins for Kubernetes, CI/CD, cloud providers, and more
  • Search Platform — Unified search across all catalog entities, docs, and plugin data
Adoption Data 2026 CNCF Survey
Enterprise Adoption Statistics

As of 2026, Backstage powers developer portals at 3,000+ companies including Netflix, American Airlines, HP, Expedia, and IKEA. The project has 1,200+ contributors and 200+ open-source plugins in the official repository. Organizations report 40-60% reduction in developer onboarding time and 25% improvement in mean time to first deployment after implementing Backstage.

CNCF Platform Engineering Developer Experience

Architecture

Backstage follows a modular, plugin-based architecture with clear separation between the frontend user interface, backend API layer, and data persistence. Understanding this architecture is essential for production deployments and custom plugin development.

Backstage High-Level Architecture
flowchart TB
    subgraph Frontend["Frontend (React SPA)"]
        UI[Backstage UI Shell]
        FP[Frontend Plugins]
        Theme[Theme Engine]
    end

    subgraph Backend["Backend (Node.js)"]
        API[Backend API]
        BP[Backend Plugins]
        Scaffolder[Scaffolder Engine]
        Auth[Auth Provider]
        Search[Search Engine]
    end

    subgraph Data["Data Layer"]
        PG[(PostgreSQL)]
        Cache[("Cache (Redis)")]
    end

    subgraph External["External Services"]
        GH[GitHub / GitLab]
        K8s[Kubernetes]
        CI[CI/CD Systems]
        Cloud[Cloud Providers]
    end

    UI --> API
    FP --> BP
    API --> PG
    API --> Cache
    BP --> GH
    BP --> K8s
    Scaffolder --> GH
    Auth --> GH
    Search --> PG
            

Frontend Layer

The frontend is a React single-page application (SPA) built with Material-UI and Backstage's own component library. It provides a plugin-aware shell where each plugin renders its own React components into designated extension points. The frontend uses a composable routing system where plugins register their pages and navigation items.

Backend Layer

The backend is a Node.js application using Express.js, organized into backend plugins that each expose their own REST or GraphQL APIs. The backend handles service discovery, authentication token management, catalog processing, scaffolder execution, and TechDocs generation. Each backend plugin runs within the same process but maintains isolated router namespaces.

Database

PostgreSQL is the recommended production database, storing catalog entities, user sessions, search indices, and plugin-specific data. SQLite is supported for development but not for production workloads. Each backend plugin manages its own database schema through Knex.js migrations.

Plugin Isolation: Each plugin (frontend and backend) is an independent npm package with its own dependencies, routes, and database tables. This isolation enables teams to develop, test, and deploy plugins independently — a critical requirement for large organizations.

Installation & Setup

Backstage provides a CLI scaffolder that creates a new application instance with all required configuration. The generated project is a monorepo containing both frontend and backend packages.

Prerequisites

  • Node.js 18+ (LTS recommended)
  • Yarn 1.x (Classic) or Yarn 3+ (Berry)
  • Docker (for containerized deployment)
  • PostgreSQL 12+ (production)
  • Git 2.x+

Creating a New Backstage App

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

# Follow the interactive prompts:
# ? Enter a name for the app [my-backstage-app]
# ? Select database for the backend [PostgreSQL]

# Navigate into the project
cd my-backstage-app

# Install dependencies
yarn install

# Start the development server (frontend + backend)
yarn dev

The development server starts the frontend on port 3000 and the backend on port 7007. The app uses SQLite by default in development mode — no external database required for local development.

Project Structure

my-backstage-app/
├── app-config.yaml              # Main configuration file
├── app-config.production.yaml   # Production overrides
├── catalog-info.yaml            # Self-referencing catalog entry
├── packages/
│   ├── app/                     # Frontend application
│   │   ├── src/
│   │   │   ├── App.tsx          # Plugin registration & routing
│   │   │   └── components/     # Custom components
│   │   └── package.json
│   └── backend/                 # Backend application
│       ├── src/
│       │   ├── index.ts         # Backend entry point
│       │   └── plugins/         # Backend plugin wiring
│       └── package.json
├── plugins/                     # Custom plugins (local development)
└── packages.json                # Root workspace config
Quick Verification: After running yarn dev, visit http://localhost:3000 to see the Backstage UI. The software catalog should be empty initially. Visit http://localhost:7007/api/catalog/entities to verify the backend API is responding.

Software Catalog

The Software Catalog is the backbone of Backstage — it provides a centralized registry of all software assets in your organization. Every service, library, website, data pipeline, and ML model can be registered as a catalog entity with rich metadata including ownership, lifecycle stage, dependencies, and API specifications.

Entity Model

Backstage Entity Relationship Model
flowchart LR
    Domain["Domain
(Business Area)"] --> System["System
(Product)"] System --> Component["Component
(Service/Library)"] Component --> API["API
(Interface)"] Group["Group
(Team)"] -->|owns| Component User["User
(Developer)"] -->|memberOf| Group Component -->|dependsOn| Component Component -->|providesApi| API Component -->|consumesApi| API

catalog-info.yaml

Every entity is defined by a catalog-info.yaml file stored alongside the source code. This file follows the Backstage entity descriptor format:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-service
  description: Handles payment processing and transaction management
  labels:
    tier: critical
  annotations:
    github.com/project-slug: myorg/payment-service
    backstage.io/techdocs-ref: dir:.
    pagerduty.com/service-id: PXXXXXX
  tags:
    - java
    - spring-boot
    - payments
  links:
    - url: https://grafana.internal/d/payments
      title: Grafana Dashboard
      icon: dashboard
spec:
  type: service
  lifecycle: production
  owner: team-payments
  system: checkout-system
  dependsOn:
    - component:user-service
    - resource:payments-db
  providesApis:
    - payment-api
  consumesApis:
    - user-api
    - notification-api

Entity Kinds

Kind Purpose Example
Component Individual software piece (service, library, website) payment-service, ui-library
API Interface boundary (REST, gRPC, event) payment-api (OpenAPI spec)
System Collection of components forming a product checkout-system
Domain Business area grouping systems commerce-domain
Group Team or organizational unit team-payments
User Individual person john.doe
Resource Infrastructure (database, S3 bucket) payments-db

Registering Entities

Configure catalog locations in app-config.yaml to automatically discover and ingest entities:

# app-config.yaml - Catalog configuration
catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration
  rules:
    - allow: [Component, System, API, Resource, Location, Domain, Group, User]
  locations:
    # Register individual repos
    - type: url
      target: https://github.com/myorg/payment-service/blob/main/catalog-info.yaml
    # Discover all repos in an org (GitHub discovery)
    - type: github-discovery
      target: https://github.com/myorg/*/blob/main/catalog-info.yaml
    # Register org structure (teams & users)
    - type: github-org
      target: https://github.com/myorg
    # Static YAML for shared definitions
    - type: file
      target: ../../catalog/systems.yaml

TechDocs (Docs as Code)

TechDocs is Backstage's built-in documentation system that follows the "docs-as-code" philosophy — documentation lives alongside source code in the same repository, written in Markdown, and rendered directly within the Backstage portal. Under the hood, TechDocs uses MkDocs with the Material theme to generate static HTML that is served through the Backstage frontend.

Important: TechDocs requires mkdocs, mkdocs-techdocs-core, and python3 installed on the machine that generates documentation. In CI/CD pipelines, use the official spotify/techdocs Docker image which includes all dependencies.

TechDocs Configuration

# app-config.yaml - TechDocs configuration
techdocs:
  builder: 'local'        # 'local' for dev, 'external' for CI/CD
  generator:
    runIn: 'local'         # 'local' or 'docker'
  publisher:
    type: 'local'          # 'local', 'googleGcs', 'awsS3', 'azureBlobStorage'
    # For production with S3:
    # type: 'awsS3'
    # awsS3:
    #   bucketName: 'my-techdocs-bucket'
    #   region: 'us-east-1'

Adding TechDocs to a Component

Each component that provides documentation needs a mkdocs.yml at its root and the TechDocs annotation in its catalog descriptor:

# mkdocs.yml - placed in the service repository root
site_name: Payment Service Documentation
nav:
  - Home: index.md
  - Architecture: architecture.md
  - API Reference: api-reference.md
  - Runbooks:
    - Incident Response: runbooks/incident-response.md
    - Scaling: runbooks/scaling.md
  - ADRs:
    - ADR-001 Database Choice: adrs/001-database-choice.md

plugins:
  - techdocs-core

markdown_extensions:
  - admonition
  - codehilite
  - pymdownx.superfences:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format

Documentation files live in a docs/ directory alongside the code, making it natural for developers to update docs as part of their pull requests.

Software Templates (Golden Paths)

Software Templates — often called "Golden Paths" — are Backstage's mechanism for creating new software projects with organizational best practices baked in from day one. Instead of developers cloning a template repo and manually configuring CI/CD, observability, and infrastructure, templates automate the entire process through a guided wizard interface.

Best Practice Platform Engineering Pattern
Golden Path Design Principles

Effective golden paths follow three principles: (1) Opinionated but flexible — provide sensible defaults with escape hatches for advanced users; (2) End-to-end — a template should produce a fully deployed, observable service, not just a code skeleton; (3) Maintained — templates need regular updates as tooling evolves. Organizations that treat templates as products (with owners, versioning, and deprecation cycles) see 3x higher adoption rates.

Golden Paths Developer Experience Self-Service

Template Structure

# template.yaml - Backstage Software Template
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: spring-boot-service
  title: Spring Boot Microservice
  description: Creates a production-ready Spring Boot service with CI/CD, monitoring, and K8s deployment
  tags:
    - java
    - spring-boot
    - recommended
spec:
  owner: team-platform
  type: service

  # Input parameters shown in the wizard UI
  parameters:
    - title: Service Configuration
      required:
        - name
        - owner
        - description
      properties:
        name:
          title: Service Name
          type: string
          description: Unique name for the service (lowercase, hyphens allowed)
          pattern: '^[a-z][a-z0-9-]*$'
          ui:autofocus: true
        description:
          title: Description
          type: string
          description: Brief description of what this service does
        owner:
          title: Owner
          type: string
          description: Team that owns this service
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group

    - title: Technical Options
      properties:
        javaVersion:
          title: Java Version
          type: string
          enum: ['17', '21']
          default: '21'
        database:
          title: Database
          type: string
          enum: ['postgresql', 'mysql', 'none']
          default: 'postgresql'
        includeKafka:
          title: Include Kafka Integration
          type: boolean
          default: false

    - title: Repository Configuration
      required:
        - repoUrl
      properties:
        repoUrl:
          title: Repository Location
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - github.com
            allowedOwners:
              - myorg

  # Scaffolder actions executed in order
  steps:
    - id: fetch-template
      name: Fetch Template Skeleton
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          javaVersion: ${{ parameters.javaVersion }}
          database: ${{ parameters.database }}

    - id: publish-repo
      name: Publish to GitHub
      action: publish:github
      input:
        allowedHosts: ['github.com']
        repoUrl: ${{ parameters.repoUrl }}
        description: ${{ parameters.description }}
        defaultBranch: main
        protectDefaultBranch: true
        requireCodeOwnerReviews: true

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

    - id: create-argocd-app
      name: Create ArgoCD Application
      action: argocd:create-resources
      input:
        appName: ${{ parameters.name }}
        argoInstance: production
        namespace: ${{ parameters.name }}
        repoUrl: ${{ steps['publish-repo'].output.remoteUrl }}
        path: deploy/k8s

  # Output shown after completion
  output:
    links:
      - title: Repository
        url: ${{ steps['publish-repo'].output.remoteUrl }}
      - title: Open in Backstage Catalog
        icon: catalog
        entityRef: ${{ steps['register-catalog'].output.entityRef }}
      - title: ArgoCD Application
        url: https://argocd.internal/applications/${{ parameters.name }}

Plugins Ecosystem

Backstage's plugin architecture is its greatest strength — every feature is a plugin, including the core catalog, TechDocs, and scaffolder. This design means the platform can be extended infinitely without modifying core code. Plugins come in three forms: frontend plugins (React UI), backend plugins (Node.js APIs), and full-stack plugins (both).

Core Plugins (Included by Default)

  • @backstage/plugin-catalog — Software Catalog browser and entity pages
  • @backstage/plugin-techdocs — Documentation viewer and search
  • @backstage/plugin-scaffolder — Template wizard and execution engine
  • @backstage/plugin-search — Unified search across all data sources
  • @backstage/plugin-auth-backend — Authentication providers and session management

Popular Community Plugins

Plugin Category Purpose
@backstage/plugin-kubernetes Infrastructure View pods, deployments, services for each component
@roadiehq/backstage-plugin-github-actions CI/CD View GitHub Actions workflow runs and status
@backstage/plugin-cost-insights FinOps Cloud cost visibility per team and service
@pagerduty/backstage-plugin Incident Mgmt On-call schedules and active incidents per service
@backstage-community/plugin-sonarqube Code Quality SonarQube metrics and quality gates
@roadiehq/backstage-plugin-argo-cd GitOps ArgoCD application sync status and history

Installing a Plugin

# Install the Kubernetes plugin (frontend + backend)
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend

# Install a community plugin
yarn --cwd packages/app add @roadiehq/backstage-plugin-github-actions

After installation, plugins must be wired into the application. Frontend plugins are registered in packages/app/src/App.tsx and backend plugins in the backend's plugin configuration:

// packages/app/src/App.tsx - Register frontend plugin route
import { KubernetesPage } from '@backstage/plugin-kubernetes';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';

// Add to the entity page component
const serviceEntityPage = (
  <EntityLayout>
    <EntityLayout.Route path="/" title="Overview">
      <EntityOverviewContent />
    </EntityLayout.Route>
    <EntityLayout.Route path="/kubernetes" title="Kubernetes">
      <EntityKubernetesContent />
    </EntityLayout.Route>
    <EntityLayout.Route path="/ci-cd" title="CI/CD">
      <EntityGithubActionsContent />
    </EntityLayout.Route>
  </EntityLayout>
);

Authentication & Authorization

Backstage provides a flexible authentication framework supporting multiple identity providers simultaneously. Authentication handles user identity (who you are), while the permission framework controls authorization (what you can do).

Configuring GitHub OAuth

# app-config.yaml - Authentication configuration
auth:
  environment: production
  providers:
    github:
      production:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
        signIn:
          resolvers:
            - resolver: usernameMatchingUserEntityName
    google:
      production:
        clientId: ${AUTH_GOOGLE_CLIENT_ID}
        clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
        signIn:
          resolvers:
            - resolver: emailMatchingUserEntityProfileEmail
Security Best Practice: Always use environment variables for OAuth secrets — never commit credentials to source control. In Kubernetes deployments, use Sealed Secrets or an external secrets operator to inject credentials at runtime.

Permission Framework (RBAC)

The Backstage permission framework enables fine-grained access control. Permissions are defined per-plugin and evaluated by a central policy decision point:

// packages/backend/src/plugins/permission.ts
import { createRouter } from '@backstage/plugin-permission-backend';
import {
  AuthorizeResult,
  PolicyDecision,
  isPermission,
} from '@backstage/plugin-permission-common';
import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/alpha';

class CustomPermissionPolicy implements PermissionPolicy {
  async handle(
    request: PolicyQuery,
    user?: PolicyQueryUser,
  ): Promise<PolicyDecision> {
    // Only platform-team can delete catalog entities
    if (isPermission(request.permission, catalogEntityDeletePermission)) {
      const isAdmin = user?.ownership?.ownershipEntityRefs?.some(
        ref => ref === 'group:default/platform-team'
      );
      return {
        result: isAdmin
          ? AuthorizeResult.ALLOW
          : AuthorizeResult.DENY,
      };
    }
    // Default: allow everything else
    return { result: AuthorizeResult.ALLOW };
  }
}

GitHub Integration

GitHub is the most common integration point for Backstage, providing authentication, catalog discovery, template publishing, and organizational structure import. A complete GitHub integration requires both an OAuth App (for user authentication) and a GitHub App or Personal Access Token (for API access).

Setting Up GitHub Integration

# app-config.yaml - GitHub integration
integrations:
  github:
    - host: github.com
      # Option 1: GitHub App (recommended for organizations)
      apps:
        - appId: ${GITHUB_APP_ID}
          webhookUrl: https://backstage.example.com/api/github/webhook
          clientId: ${GITHUB_APP_CLIENT_ID}
          clientSecret: ${GITHUB_APP_CLIENT_SECRET}
          webhookSecret: ${GITHUB_WEBHOOK_SECRET}
          privateKey: ${GITHUB_APP_PRIVATE_KEY}
      # Option 2: Personal Access Token (simpler, less secure)
      # token: ${GITHUB_TOKEN}

# Organization catalog discovery
catalog:
  providers:
    github:
      myOrg:
        organization: 'my-github-org'
        catalogPath: '/catalog-info.yaml'
        filters:
          branch: 'main'
          repository: '.*'          # All repos
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }
GitHub App vs PAT: GitHub Apps are strongly recommended for production deployments. They provide higher API rate limits (5,000 requests/hour vs 5,000/hour per user), granular repository permissions, and organizational-level installation. PATs are tied to individual users and create single-point-of-failure risks.

Production Deployment

Moving Backstage from development to production requires building optimized Docker images, configuring PostgreSQL, setting up persistent storage for TechDocs, and deploying to Kubernetes with proper resource limits and health checks.

Building the Docker Image

# Build the backend bundle
yarn build:backend

# Build the Docker image using the included Dockerfile
docker build -t backstage:latest \
  --build-arg APP_CONFIG=app-config.production.yaml \
  -f packages/backend/Dockerfile .

# Tag and push to registry
docker tag backstage:latest registry.example.com/backstage:v1.0.0
docker push registry.example.com/backstage:v1.0.0

Production Configuration

# app-config.production.yaml
app:
  title: Internal Developer Portal
  baseUrl: https://backstage.example.com

backend:
  baseUrl: https://backstage.example.com
  listen:
    port: 7007
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      database: backstage
      ssl:
        require: true
        rejectUnauthorized: true
  cors:
    origin: https://backstage.example.com
    methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
    credentials: true

techdocs:
  builder: 'external'
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: 'backstage-techdocs-prod'
      region: 'us-east-1'
      credentials:
        roleArn: arn:aws:iam::123456789:role/backstage-techdocs

Kubernetes Deployment (Helm)

# helm/values.yaml - Backstage Helm chart values
replicaCount: 2

image:
  repository: registry.example.com/backstage
  tag: v1.0.0
  pullPolicy: IfNotPresent

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 2000m
    memory: 2Gi

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: 50m
  hosts:
    - host: backstage.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: backstage-tls
      hosts:
        - backstage.example.com

postgresql:
  enabled: true
  auth:
    existingSecret: backstage-postgres-secret
  primary:
    persistence:
      size: 20Gi
    resources:
      requests:
        cpu: 250m
        memory: 256Mi

livenessProbe:
  httpGet:
    path: /healthcheck
    port: 7007
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /healthcheck
    port: 7007
  initialDelaySeconds: 15
  periodSeconds: 5

env:
  - name: POSTGRES_HOST
    value: backstage-postgresql
  - name: POSTGRES_PORT
    value: "5432"

envFrom:
  - secretRef:
      name: backstage-secrets

Troubleshooting

Backstage deployments commonly encounter issues during initial setup and upgrades. Here are the most frequent problems and their solutions.

Plugin Version Conflicts

Backstage uses a synchronized release train — all @backstage/* packages should be on the same version. Mismatched versions cause runtime errors:

# Check for version mismatches
yarn backstage-cli versions:check

# Bump all Backstage packages to latest compatible versions
yarn backstage-cli versions:bump

# If conflicts persist, clear and reinstall
rm -rf node_modules yarn.lock
yarn install

Database Migration Failures

Migration Fix: If database migrations fail during startup, check that the PostgreSQL user has CREATE TABLE and ALTER TABLE permissions. For stuck migrations, check the knex_migrations_lock table — a locked row from a failed previous attempt will block all subsequent migrations. Delete the lock row: DELETE FROM knex_migrations_lock WHERE is_locked = 1;

Common Issues & Solutions

Issue Cause Solution
Auth redirect loops Mismatched baseUrl between frontend/backend Ensure app.baseUrl and backend.baseUrl use the same protocol and domain
Catalog entities not appearing YAML validation errors Check backend logs for entity processing errors; validate YAML with backstage-cli catalog:validate
TechDocs build failures Missing MkDocs plugins Install mkdocs-techdocs-core plugin; use Docker-based generation in CI
Template scaffold 403 errors Insufficient GitHub App permissions Ensure GitHub App has Contents: Write and Administration: Write on target repos
High memory usage Catalog processing large orgs Set catalog.processingInterval to 5+ minutes; add catalog.rules filters