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