Composing a Portal
A DevPortal installation out of the box is a service catalog and a template runner. Teams can register their services, create new ones from templates, and browse the software landscape. That is Day-0: the portal knows what exists and can create things, but it doesn't connect to anything live yet.
The value engineers actually care about — seeing pod status, triggering a deployment, checking code quality, browsing Grafana dashboards without leaving the service page — comes from plugin composition. Understanding how composition works is what separates "I ran the YAML" from "I built an operational hub."
Three levels of composition
Every plugin activates across three layers. All three must be in place before a developer sees anything.
1. Load — dynamic-plugins.yaml
This controls which plugins are present at all. Flip disabled: false for a bundled plugin, or add an OCI reference for an external one. No image rebuild required.
plugins:
- package: ./dynamic-plugins/dist/backstage-plugin-kubernetes-dynamic
disabled: false
Enabling a plugin here loads its code. UI components are available. But nothing appears to the developer yet.
2. Context — entity annotations
Plugins are context-aware by design. They do not add global tabs. They add tabs and cards to specific catalog entities — and only when those entities carry the right annotation.
# catalog-info.yaml
metadata:
annotations:
backstage.io/kubernetes-label-selector: 'app=my-service'
Without this annotation on the entity, the Kubernetes plugin is loaded but idle. With it, the Kubernetes tab appears on that entity only, and shows pods matching the selector.
This is intentional. A platform with dozens of services doesn't need every plugin visible on every entity. Each service declares what it uses. The portal surfaces exactly what belongs to that service.
3. Backend — app-config.yaml
The tab appears. But it needs to know where to fetch data from. The backend configuration provides that:
kubernetes:
clusterLocatorMethods:
- type: config
clusters:
- name: production
url: ${K8S_CLUSTER_URL}
serviceAccountToken: ${K8S_CLUSTER_TOKEN}
Without this, the tab loads and shows an error or empty state. With all three layers in place, the tab displays live data.