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-]*$'
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.
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.