Modeling Ownership with Backstage Groups
Ownership is the backbone of a portal: it decides who gets paged, who can edit an entity, and who a cost report is addressed to. Get the Group model right and every downstream feature — RBAC, on-call, notifications — resolves cleanly; get it wrong and each of those inherits the mess. This how-to models teams as Backstage Group entities with sensible hierarchy and ownership edges. It extends Team & Permission Models within Authentication, RBAC & Security Governance.
Prerequisites
- Backstage 1.20+ with the catalog populated with services.
- A source of truth for teams — an org chart, an HR feed, or directory groups from Mapping LDAP Groups to Portal Roles.
- Agreement on what a “team” is: the unit that owns services and carries on-call, not every ad-hoc chat channel.
- A convention for group naming so references stay stable as the org changes.
Exact Configuration
-
Model teams as Group entities with a hierarchy that mirrors the org, using
parentandchildren.# groups.yaml apiVersion: backstage.io/v1alpha1 kind: Group metadata: name: payments spec: type: team parent: fintech children: [] members: [asmith, bjones] -
Attach ownership on the service, pointing
spec.ownerat the group, not an individual.# catalog-info.yaml kind: Component metadata: name: payments-api spec: type: service owner: group:default/payments lifecycle: production -
Keep membership fresh by ingesting from the directory rather than hand-editing member lists.
# app-config.yaml — reuse the LDAP/Entra ingestion so members stay current catalog: providers: ldap: default: groups: dn: ou=groups,dc=example,dc=com
Validation
# Requires curl, jq
# 1. Every service resolves to a Group owner, not a person
curl -s "${PORTAL_URL}/api/catalog/entities?filter=kind=component" \
| jq '[.[] | select(.spec.owner | startswith("group:") | not) | .metadata.name]'
# Expected: [] — no service owned by an individual
# 2. Group hierarchy resolves parents and children
curl -s "${PORTAL_URL}/api/catalog/entities/by-name/group/default/payments" \
| jq '{parent: .spec.parent, members: (.spec.members | length)}'
# Expected: a parent and a non-zero member count
# 3. No orphaned owners (owner group actually exists)
curl -s "${PORTAL_URL}/api/catalog/entities?filter=kind=component" \
| jq -r '.[].spec.owner' | sort -u
# Expected: every value maps to a real Group entity
Edge Cases & Troubleshooting
| Symptom | Root Cause | Resolution |
|---|---|---|
| Service owned by a person | spec.owner set to a User |
Repoint to the owning Group |
| Owner group does not exist | Group deleted or renamed | Recreate or update the reference; enforce in CI |
| Membership always stale | Members hand-edited in YAML | Ingest members from the directory instead |
| On-call resolves nobody | Group has no members | Ensure ingestion populates spec.members |
| Reorg breaks references | Group renamed | Keep stable names; alias old names during transition |
Frequently Asked Questions
Should ownership ever point at an individual?
Almost never for a service. People change teams and leave; a group persists and its membership updates. The rare exception is a genuinely personal artifact — a scratch project — and even then a group of one is more future-proof than a raw user reference.
How deep should the group hierarchy go?
As shallow as your org allows while still letting escalation and reporting roll up. Two or three levels — team, department, org — is plenty for most companies. Deep trees make ownership queries slow and confuse escalation; model the levels that carry real responsibility and stop there.