Learnixo
Back to blog
Cloud & DevOpsadvanced

Platform Engineering: Backstage Deep Dive — Software Catalog, Scaffolder Templates, TechDocs, and Plugins

Build a production Internal Developer Portal with Backstage — software catalog with YAML descriptors, Scaffolder golden path templates that provision real services, TechDocs as code, custom plugins for internal tooling, and catalog accuracy strategies.

LearnixoJune 11, 20269 min read
Platform EngineeringBackstageDeveloper PortalIDPGolden PathSoftware CatalogKubernetes
Share:𝕏

What Backstage Solves

Large engineering organizations have a discovery problem: hundreds of services, no canonical list of what exists, who owns it, where the docs are, how to deploy it, or what it depends on. Engineers spend time asking Slack instead of building.

Backstage is Spotify's open-source Internal Developer Portal — a single place to:

  • Discover all services, APIs, libraries, and infrastructure
  • Create new services via golden path templates (scaffolder)
  • Document everything in Markdown (TechDocs)
  • Track service health, ownership, and compliance
  • Extend with plugins for any internal tool (Argo CD, PagerDuty, Kubernetes, cost)

Getting Started: Deploy Backstage

Bash
# Create a new Backstage app
npx @backstage/create-app@latest
cd my-backstage-app

# Run locally
yarn dev

For Kubernetes deployment:

DOCKERFILE
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn tsc
RUN yarn build:backend

FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=build /app/packages/backend/dist/ ./
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "index.js"]
YAML
# backstage-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backstage
  namespace: backstage
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: backstage
          image: registry.example.com/backstage:latest
          env:
            - name: APP_CONFIG_app_baseUrl
              value: "https://portal.example.com"
            - name: GITHUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: backstage-secrets
                  key: github-token
            - name: POSTGRES_HOST
              value: backstage-db.backstage.svc
          ports:
            - containerPort: 7007

Software Catalog: catalog-info.yaml

Every service, API, library, and team is described by a catalog-info.yaml file at the root of the repository. Backstage discovers these via GitHub/GitLab integrations.

Service (Component)

YAML
# catalog-info.yaml  in the root of the payment-service repo
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-service
  title: Payment Service
  description: Handles payment processing, refunds, and billing
  annotations:
    # Backstage plugin integrations
    github.com/project-slug: org/payment-service
    backstage.io/techdocs-ref: dir:.       # TechDocs in this repo
    argo-cd/app-name: payment-service-production
    pagerduty.com/service-id: PABC123
    backstage.io/kubernetes-id: payment-service
    # Cost tracking
    finops.example.com/cost-center: "payments-team"
  links:
    - url: https://grafana.example.com/d/payment-service
      title: Grafana Dashboard
      icon: dashboard
    - url: https://runbooks.example.com/payment-service
      title: Runbook
      icon: docs
  tags:
    - payment
    - critical
    - typescript
spec:
  type: service
  lifecycle: production
  owner: group:payments-team
  system: checkout-system
  dependsOn:
    - component:inventory-service
    - component:notification-service
    - resource:orders-db
  providesApis:
    - payment-api

API descriptor

YAML
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: payment-api
  description: REST API for payment processing
spec:
  type: openapi
  lifecycle: production
  owner: group:payments-team
  definition:
    $text: ./openapi.yaml   # local OpenAPI spec

Backstage renders the OpenAPI spec as interactive documentation in the catalog.

System: group related components

YAML
# A System groups components that deliver a user-facing capability
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
  name: checkout-system
  description: End-to-end checkout flow (cart, payment, inventory, notifications)
spec:
  owner: group:checkout-team
  domain: domain:ecommerce

Autodiscovery from GitHub

YAML
# app-config.yaml  discover catalog-info.yaml from all repos
catalog:
  providers:
    github:
      org:
        organization: my-org
        catalogPath: /catalog-info.yaml
        filters:
          branch: main
          repository: .*          # all repos
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

Backstage scans every GitHub repo every 30 minutes. Any repo with catalog-info.yaml is registered automatically. No manual registration needed.


Scaffolder: Golden Path Templates

The Scaffolder creates new services from templates — a Git repo, Kubernetes manifests, ArgoCD application, and catalog registration all in one click.

Template anatomy

YAML
# template.yaml  the golden path for a new TypeScript service
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: typescript-service
  title: TypeScript Microservice
  description: Create a new TypeScript service with Docker, Kubernetes manifests, and ArgoCD
  tags:
    - typescript
    - recommended
spec:
  owner: group:platform-team
  type: service

  # Step 1: Input form  shown to the developer
  parameters:
    - title: Service Details
      required: [name, owner, description]
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z][a-z0-9-]*$'
          description: "Lowercase, hyphens only (e.g., order-processor)"
          ui:autofocus: true
        description:
          title: Description
          type: string
        owner:
          title: Team
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
        system:
          title: System
          type: string
          ui:field: EntityPicker
          ui:options:
            catalogFilter:
              kind: System

    - title: Infrastructure
      properties:
        needsDatabase:
          title: Provision a PostgreSQL database?
          type: boolean
          default: false
        databaseSize:
          title: Database size
          type: string
          enum: [small, medium, large]
          default: small
          ui:widget: select

    - title: Repository
      required: [repoUrl]
      properties:
        repoUrl:
          title: Repository Location
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - github.com
            allowedOrganizations:
              - my-org

  # Step 2: Actions  what Backstage does
  steps:
    # 1. Fetch the skeleton (template files)
    - id: fetch-template
      name: Fetch template files
      action: fetch:template
      input:
        url: ./skeleton           # folder with template files
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          system: ${{ parameters.system }}
          needsDatabase: ${{ parameters.needsDatabase }}
          databaseSize: ${{ parameters.databaseSize }}

    # 2. Create the GitHub repository
    - id: create-repo
      name: Create GitHub repository
      action: publish:github
      input:
        allowedHosts: ['github.com']
        repoUrl: ${{ parameters.repoUrl }}
        description: ${{ parameters.description }}
        defaultBranch: main
        gitCommitMessage: "chore: initial scaffold from platform template"
        topics:
          - ${{ parameters.system }}
          - typescript

    # 3. Create ArgoCD Application
    - id: create-argocd-app
      name: Register in ArgoCD
      action: argocd:create-application
      input:
        appName: ${{ parameters.name }}
        argoInstance: production
        namespace: ${{ parameters.name }}
        repoUrl: ${{ steps['create-repo'].output.remoteUrl }}
        path: k8s/overlays/production

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

    # 5. (Optional) Provision database via Crossplane
    - id: provision-database
      name: Provision PostgreSQL database
      if: ${{ parameters.needsDatabase }}
      action: kubernetes:apply
      input:
        namespace: ${{ parameters.name }}
        manifest: |
          apiVersion: platform.example.com/v1alpha1
          kind: PostgresDatabase
          metadata:
            name: ${{ parameters.name }}-db
            namespace: ${{ parameters.name }}
          spec:
            parameters:
              size: ${{ parameters.databaseSize }}
              environment: production

  # Step 3: Output links shown after creation
  output:
    links:
      - title: Repository
        url: ${{ steps['create-repo'].output.remoteUrl }}
      - title: ArgoCD Application
        url: https://argocd.example.com/applications/${{ parameters.name }}
      - title: Catalog Entry
        icon: catalog
        entityRef: ${{ steps['register-catalog'].output.entityRef }}

Template skeleton files

The ./skeleton folder contains templated files with ${{ values.* }} substitutions:

skeleton/
├── catalog-info.yaml         # auto-registered
├── README.md
├── src/
│   └── index.ts
├── Dockerfile
├── package.json
└── k8s/
    ├── base/
    │   ├── deployment.yaml
    │   ├── service.yaml
    │   └── kustomization.yaml
    └── overlays/
        ├── staging/
        └── production/
YAML
# skeleton/catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: ${{ values.name }}
  description: ${{ values.description }}
  annotations:
    github.com/project-slug: my-org/${{ values.name }}
    backstage.io/techdocs-ref: dir:.
spec:
  type: service
  lifecycle: production
  owner: ${{ values.owner }}
  system: ${{ values.system }}

A developer fills the form → Backstage creates the repo, creates the namespace, registers it in ArgoCD, provisions the database (if requested), and registers the catalog entry — in under 2 minutes.


TechDocs: Documentation as Code

TechDocs renders Markdown (MkDocs) from the service repo directly in Backstage. No separate wiki required.

Setup

YAML
# In the service repo: mkdocs.yml
site_name: Payment Service
site_description: Handles payment processing, refunds, and billing
docs_dir: docs/

nav:
  - Overview: index.md
  - Architecture: architecture.md
  - API Reference: api.md
  - Runbooks:
      - Deploy: runbooks/deploy.md
      - Incident Response: runbooks/incidents.md
  - ADRs:
      - "ADR-001: Use Stripe": adrs/001-stripe.md
      - "ADR-002: Idempotency keys": adrs/002-idempotency.md

plugins:
  - techdocs-core
YAML
# catalog-info.yaml annotation
annotations:
  backstage.io/techdocs-ref: dir:.   # docs/ folder in this repo

Backstage fetches, builds, and serves the MkDocs site. Docs live in the same repo as the code — they get reviewed in PRs, stay in sync with the code, and are searchable from the catalog.


Kubernetes Plugin: Real-Time Service Status

The @backstage/plugin-kubernetes plugin shows live Kubernetes status in the catalog:

YAML
# app-config.yaml
kubernetes:
  serviceLocatorMethod:
    type: multiTenant
  clusterLocatorMethods:
    - type: config
      clusters:
        - url: https://prod-eu.example.com
          name: production-eu
          authProvider: serviceAccount
          serviceAccountToken: ${K8S_SA_TOKEN_PROD_EU}
          caData: ${K8S_CA_PROD_EU}
        - url: https://prod-us.example.com
          name: production-us
          authProvider: serviceAccount
          serviceAccountToken: ${K8S_SA_TOKEN_PROD_US}

Backstage matches components to Kubernetes resources via the backstage.io/kubernetes-id annotation. On the component page: live pod count, restart count, CPU/memory usage, recent events — across all clusters simultaneously.


ArgoCD Plugin: Deployment Status in Catalog

Bash
yarn workspace app add @roadiehq/backstage-plugin-argo-cd
yarn workspace backend add @roadiehq/backstage-plugin-argo-cd-backend
YAML
# app-config.yaml
argocd:
  baseUrl: https://argocd.example.com
  token: ${ARGOCD_TOKEN}
  appLocatorMethods:
    - type: config
      instances:
        - name: production
          url: https://argocd.example.com
          token: ${ARGOCD_TOKEN}

Each component's Backstage page shows ArgoCD sync status, last deploy time, current image tag, and sync history — no need to open ArgoCD for a quick status check.


Catalog Accuracy: Keeping It Fresh

The #1 failure mode for Backstage: catalog entries that go stale. Services get decommissioned but their catalog-info.yaml stays in Git. Owners change but annotations aren't updated.

Automated catalog validation with Kyverno

YAML
# Enforce catalog-info.yaml exists in every repo (via CI check)
# .github/workflows/catalog-check.yml
name: Catalog Validation
on: [push, pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate catalog-info.yaml
        run: |
          if [ ! -f catalog-info.yaml ]; then
            echo "ERROR: catalog-info.yaml missing"
            exit 1
          fi
          # Validate schema
          npx @backstage/catalog-validator catalog-info.yaml

Lifecycle labels for decommissioning

YAML
spec:
  lifecycle: deprecated    # shows warning banner in Backstage
  # or
  lifecycle: experimental  # not yet production
  # or
  lifecycle: production    # active

Use deprecated before deleting a service. Backstage shows a banner and tools can query "all deprecated services" for cleanup tracking.

Automated ownership check

Run a weekly job that:

  1. Queries all Backstage components: GET /api/catalog/entities?filter=kind=Component
  2. Checks each owner group exists in the catalog
  3. Checks each repo still exists in GitHub
  4. Posts a Slack report of stale/orphaned catalog entries to the platform team

Platform Home Page

Backstage's @backstage/plugin-home plugin creates a customizable home page:

TYPESCRIPT
// packages/app/src/components/home/HomePage.tsx
export const HomePage = () => (
  <Page themeId="home">
    <Content>
      <Grid container spacing={3}>
        <Grid item xs={12} md={6}>
          <WelcomeTitle />
          <SearchBar />
        </Grid>
        <Grid item xs={12} md={6}>
          <HomePageStarredEntities />
        </Grid>
        <Grid item xs={12} md={6}>
          <HomePageRecentlyVisited />
        </Grid>
        <Grid item xs={12} md={4}>
          {/* Quick links to scaffolder templates */}
          <InfoCard title="Create New">
            <CreateComponentButton />
          </InfoCard>
        </Grid>
        <Grid item xs={12} md={8}>
          {/* Company announcements */}
          <AnnouncementsCard />
        </Grid>
      </Grid>
    </Content>
  </Page>
);

Measuring Backstage ROI

Track these metrics to demonstrate value to leadership:

Adoption:
  - Active users per week (Backstage analytics)
  - % of services with catalog entries
  - % of services with TechDocs

Developer productivity:
  - Time to create a new service (Scaffolder: before → after)
  - P50/P95 time from "idea" to "first deploy" (tracked via ArgoCD)
  - "Time to find the owner of X" (survey — target: < 2 minutes)

Platform quality:
  - % of services with Grafana dashboard
  - % of services with runbooks
  - % of services with owner group
  - % of deprecated services cleaned up within 30 days

Golden path adoption:
  - Services created via Scaffolder vs created manually
  - Scaffold template usage breakdown (which templates are most used)

Present these in quarterly platform reviews. The goal: Backstage should reduce the time engineers spend on meta-work (finding things, asking Slack, reading stale wikis) by 50%+.

Enjoyed this article?

Explore the Cloud & DevOps learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.