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.
# 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 }}
/>
);
};
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-]*