Pavan Rangani

HomeBlogBackstage Developer Portal: Building a Service Catalog with Custom Plugins

Backstage Developer Portal: Building a Service Catalog with Custom Plugins

By Pavan Rangani · March 25, 2026 · DevOps & Cloud

Backstage Developer Portal: Building a Service Catalog with Custom Plugins

Backstage Developer Portal Setup Guide

Backstage developer portal plugins sit at the centre of one of the most influential ideas in modern software engineering: that internal tooling deserves the same product discipline as customer-facing software. Originally created at Spotify and donated to the CNCF, Backstage now powers developer experience at thousands of organizations. It provides a unified service catalog, a documentation hub, and an extensible plugin ecosystem that eliminates the fragmentation of internal tooling. Instead of bookmarking a dozen dashboards, engineers get a single front door.

This guide walks you through building a production-ready Backstage instance with a populated service catalog, custom plugins, and software templates. Whether you are starting a platform engineering initiative or consolidating existing tools, the practical steps below help you deliver value quickly while avoiding the most common adoption traps.

Architecture Overview

Backstage consists of three core components: the frontend app (React), the backend (Node.js), and a PostgreSQL database for persistent storage. The plugin architecture allows teams to extend functionality without modifying the core platform. Crucially, the new backend system (introduced from version 1.18 onward) replaced the old hand-wired backend with a dependency-injection model, so plugins now register themselves through a small, declarative entry point rather than sprawling boilerplate.

Backstage developer portal architecture diagram
The Backstage architecture connects services, documentation, and tooling in a unified portal
# Create a new Backstage app
npx @backstage/create-app@latest my-portal
cd my-portal

# Project structure
# ├── app-config.yaml          # Main configuration
# ├── app-config.production.yaml
# ├── packages/
# │   ├── app/                 # Frontend (React)
# │   └── backend/             # Backend (Node.js)
# └── plugins/                 # Custom plugins

Configuring the Service Catalog

The service catalog is the heart of any Backstage instance. It provides a centralized registry of all software components, APIs, resources, and their ownership. Moreover, it automatically discovers and ingests catalog entities from your Git repositories, which means the registry stays accurate without a human curating a spreadsheet.

# app-config.yaml
catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration
  rules:
    - allow: [Component, System, API, Resource, Location, Group, User]
  locations:
    # GitHub org discovery
    - type: github-discovery
      target: https://github.com/my-org/*/blob/main/catalog-info.yaml
    # Static locations
    - type: url
      target: https://github.com/my-org/backstage-catalog/blob/main/all-systems.yaml
  processors:
    githubOrg:
      providers:
        - target: https://github.com
          apiBaseUrl: https://api.github.com
          orgs: ['my-org']

Each service registers itself with a catalog-info.yaml file in its repository root:

# catalog-info.yaml in each service repo
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-service
  description: Handles payment processing and billing
  annotations:
    github.com/project-slug: my-org/payment-service
    backstage.io/techdocs-ref: dir:.
    pagerduty.com/service-id: PABCDEF
    grafana/dashboard-selector: "payment-*"
  tags:
    - java
    - spring-boot
    - payments
  links:
    - url: https://grafana.internal/d/payment
      title: Grafana Dashboard
      icon: dashboard
spec:
  type: service
  lifecycle: production
  owner: team-payments
  system: billing-platform
  providesApis:
    - payment-api
  consumesApis:
    - user-api
    - notification-api
  dependsOn:
    - resource:default/payments-db
    - resource:default/payments-redis

The Entity Model: Why Relationships Matter

Beginners often treat the catalog as a flat list of services, but its real power is the relationship graph. A Component belongs to a System, which belongs to a Domain; it providesApis and consumesApis, and it dependsOn resources like databases. Because these relationships are typed, the catalog can render a live dependency diagram and answer questions like “what breaks if the payments database goes down?” In practice, teams that invest in accurate dependsOn and consumesApis edges get far more value than teams that only register bare components. The ownership field is equally important: when an incident fires, the catalog resolves owner: team-payments to a Group entity with a real on-call rotation, so nobody wastes time hunting for who owns a service.

Building Custom Backstage Developer Portal Plugins

Custom plugins are where a generic portal becomes your portal. The plugin SDK provides a structured approach to building both frontend and backend extensions. Frontend plugins contribute React routes and cards; backend plugins expose HTTP endpoints under /api/<plugin-id>. A typical pattern is a frontend dashboard backed by a small backend that aggregates data from an external system such as a cloud billing API.

# Generate a new plugin
cd my-portal
npx @backstage/cli new --select plugin
# Enter plugin ID: cost-insights
// plugins/cost-insights/src/components/CostDashboard.tsx
import React from 'react';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { Table, TableColumn } from '@backstage/core-components';

interface ServiceCost {
  name: string;
  monthlyCost: number;
  trend: number;
  owner: string;
}

export const CostDashboard = () => {
  const [costs, setCosts] = React.useState<ServiceCost[]>([]);
  const config = useApi(configApiRef);

  React.useEffect(() => {
    fetch('/api/cost-insights/services')
      .then(res => res.json())
      .then(data => setCosts(data));
  }, []);

  const columns: TableColumn<ServiceCost>[] = [
    { title: 'Service', field: 'name' },
    { title: 'Monthly Cost', field: 'monthlyCost',
      render: row => `${row.monthlyCost.toFixed(2)}` },
    { title: 'Trend', field: 'trend',
      render: row => row.trend > 0
        ? `+${row.trend}%`
        : `${row.trend}%` },
    { title: 'Owner', field: 'owner' },
  ];

  return (
    <Table
      title="Service Cost Overview"
      columns={columns}
      data={costs}
      options={{ pageSize: 20, search: true }}
    />
  );
};
Backstage plugin dashboard for cost insights
Custom plugins provide team-specific views and integrations within the portal

Entity Cards: Surfacing Plugin Data in Context

A standalone dashboard is useful, but the most valuable plugins surface data on the entity page itself. Rather than asking an engineer to navigate to a cost dashboard, you attach a cost card to every service page, filtered to that service. The backend reads the same annotations defined in catalog-info.yaml, so the integration is configuration-driven rather than hard-coded. This is the idiomatic Backstage pattern: plugins consume entity annotations, then render contextual cards. As a result, adding a new monitoring integration is often just a matter of standardising one annotation across repositories and shipping a card that reads it.

Software Templates for Golden Paths

Software templates codify best practices into self-service project scaffolding. As a result, developers can create new services, libraries, or infrastructure components that automatically follow organizational standards instead of copy-pasting from last quarter’s project:

# templates/spring-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: spring-boot-service
  title: Spring Boot Microservice
  description: Create a production-ready Spring Boot microservice
spec:
  owner: team-platform
  type: service
  parameters:
    - title: Service Details
      required: [name, owner, description]
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z][a-z0-9-]*
		
		
	


        owner:
          title: Owner Team
          type: string
          ui:field: OwnerPicker
        description:
          title: Description
          type: string
        javaVersion:
          title: Java Version
          type: string
          enum: ['17', '21']
          default: '21'
    - title: Infrastructure
      properties:
        database:
          title: Database
          type: string
          enum: ['none', 'postgresql', 'mysql']
          default: 'postgresql'
        messaging:
          title: Messaging
          type: string
          enum: ['none', 'kafka', 'rabbitmq']
  steps:
    - id: fetch
      name: Fetch Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: '{{ parameters.name }}'
          owner: '{{ parameters.owner }}'
          java_version: '{{ parameters.javaVersion }}'
    - id: publish
      name: Create Repository
      action: publish:github
      input:
        repoUrl: 'github.com?repo={{ parameters.name }}&owner=my-org'
        description: '{{ parameters.description }}'
    - id: register
      name: Register in Catalog
      action: catalog:register
      input:
        repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
        catalogInfoPath: /catalog-info.yaml

The pattern that separates a good template from a frustrating one is restraint in the parameter form. Every field you add is a decision the developer must make, so default aggressively and only expose what genuinely varies. Notice also the final catalog:register step: a template that does not register its output leaves an orphaned repository the catalog never learns about, which quietly defeats the whole purpose. For deeper context on designing these flows, see our companion piece on Golden Paths in Platform Engineering.

TechDocs Integration

TechDocs transforms Markdown documentation into a searchable knowledge base. Consequently, every service ships docs alongside its code, and those docs become accessible from the same portal. The recommended production setup uses the external builder pattern: documentation is generated in CI when a pull request merges, then published to object storage, rather than being built on demand by the Backstage backend. This keeps the portal responsive and avoids coupling page loads to a Python toolchain.

# app-config.yaml
techdocs:
  builder: 'external'
  generator:
    runIn: 'local'
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: 'my-techdocs-bucket'
      region: 'us-east-1'
      credentials:
        roleArn: 'arn:aws:iam::123456789:role/techdocs-publisher'

Authentication, RBAC, and the Permission Framework

A portal that aggregates production tooling is also a juicy target, so authentication and authorization deserve early attention. Backstage ships an auth plugin with providers for GitHub, Google, Okta, and generic OIDC, and the resolved identity maps onto User and Group entities in the catalog. On top of that, the permission framework lets you write policies that gate sensitive actions — for example, restricting who may execute a template that provisions cloud infrastructure, or who may unregister a catalog entity. A common mistake is shipping the portal with the default “everyone can do everything” policy because RBAC felt like a phase-two concern; in regulated environments that turns into an audit finding. Treat the permission policy as part of your minimum viable portal, even if it starts permissive and tightens over time.

When NOT to Use Backstage

Backstage requires significant investment in setup and maintenance, and being honest about the trade-offs will save you a painful adoption. Small teams with fewer than 20 developers may find the overhead unjustified when a simple wiki or a README-driven catalog suffices. If your organization lacks dedicated platform engineering resources, the portal tends to drift: the catalog grows stale, plugins fall behind upstream releases, and engineers route around it. Backstage is also not a monitoring solution — it aggregates links to monitoring tools but does not replace Grafana or Datadog. Finally, the framework moves quickly, so budget for ongoing dependency upgrades; teams that pin to an old version and never upgrade eventually find themselves unable to adopt new plugins. If you cannot commit at least a small standing team to own the portal as a product, a lighter-weight alternative is the more honest choice.

Platform engineering team building developer portal
Successful Backstage adoption requires dedicated platform engineering investment

Key Takeaways

  • A Backstage developer portal centralizes service discovery, documentation, and tooling in a single platform
  • The service catalog with automatic GitHub discovery eliminates manual registry maintenance, and the typed relationship graph is where the real value lives
  • Custom plugins extend the portal with team-specific features like cost insights, and entity cards surface that data in context rather than in isolated dashboards
  • Software templates enforce organizational standards while enabling developer self-service — always register the output back into the catalog
  • Wire up authentication and the permission framework early; an aggregated portal is a security-sensitive surface
  • Plan for dedicated platform engineering resources — Backstage is not a set-and-forget solution

Related Reading

External Resources

In conclusion, Backstage developer portal plugins turn a generic catalog into an opinionated platform that reflects how your organization actually builds software. By applying the patterns and practices covered in this guide — accurate catalog relationships, contextual plugins, disciplined templates, and early attention to permissions — you can build more robust, scalable, and maintainable internal tooling. Start with the fundamentals, iterate on your implementation, and continuously measure adoption to ensure you are getting real value from these approaches.

← Back to all articles