Skip to content

Backstage Primer & Modelling Guide — Developer Portal Catalogue with Linked.Archi

Engineering organizations that run Backstage end up with a rich software catalogue locked inside a single tool — hard to query alongside architecture models, hard to govern, and hard to reconcile with ArchiMate or C4. Representing the catalogue as semantic assets makes ownership, API dependencies, and system boundaries queryable across the whole architecture knowledge graph.

This guide introduces the Backstage software catalogue model as formalized in Linked.Archi, explains how its concepts map to semantic assets, and demonstrates practical modelling through a worked example.

Backstage Concepts

What is Backstage?

Backstage is an open-source developer portal platform originally created by Spotify. Its Software Catalog provides a centralized registry of all software components, systems, APIs, resources, and their ownership within an organization.

Backstage is: - Developer-centric — built for engineering teams to discover and understand services - Ownership-first — every entity has a clear owner (team/group) - API-aware — APIs are first-class citizens with provider/consumer relationships - Domain-driven — entities are grouped into business domains

"Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end." — backstage.io

Catalogue Entity Types

graph TD
    subgraph Software["Software Entities"]
        Component["Component"]
        System["System"]
        API["API"]
        Resource["Resource"]
    end

    subgraph Organisational["Organisational Entities"]
        Domain["Domain"]
        User["User"]
        Group["Group"]
    end

    Component -->|"partOfSystem"| System
    API -->|"partOfSystem"| System
    Resource -->|"partOfSystem"| System
    Component -->|"providesAPI"| API
    Component -->|"consumesAPI"| API
    Component -->|"dependsOn"| Component
    Component -->|"usesResource"| Resource
    Resource -->|"usesResource"| Resource
    Component -->|"subcomponentOf"| Component
    Component -->|"ownedBy"| Group
    System -->|"belongsToDomain"| Domain
    Domain -->|"subdomainOf"| Domain
    User -->|"memberOf"| Group
    Group -->|"childOf"| Group

    style Software fill:#B3D9FF,stroke:#333,color:#000
    style Organisational fill:#D4E6B5,stroke:#333,color:#000
Entity What it represents
Component A deployable unit — service, library, website, or application
System A set of components that provide business or technical value together
API An interface owned and maintained in the ecosystem (OpenAPI, AsyncAPI, gRPC, GraphQL)
Resource Infrastructure used by components — databases, buckets, queues, external services
Domain A high-level business domain grouping systems and components
User A human user in the ecosystem
Group A team, department, or tribe that owns entities

Relationships

The ontology declares 12 relation types, each in the three-declaration form (unqualified property, qualified class, qualified property — see the Relationship Modelling Guide):

Relationship From → To Meaning
ownedBy Any Element → Group or User Entity is owned by a team or, in smaller organisations, a person
partOfSystem Component, API, Resource → System Entity belongs to a system
providesAPI Component, System → API Component or system exposes an API
consumesAPI Component, System → API Component or system depends on an API
dependsOn Component, Resource → Component, Resource General dependency — spec.dependsOn, deliberately broad
usesResource Component, Resource → Resource Resource-facing subproperty of dependsOn, kept for continuity
belongsToDomain System → Domain System belongs to a business domain
memberOf User → Group User belongs to a team
childOf Group → Group Group organisational hierarchy, stored child-to-parent
subcomponentOf Component → Component Component nesting, e.g. a scoring sub-service packaged separately
subdomainOf Domain → Domain Domain nesting, for organisations with a large domain landscape
techdocsDelegatedTo Any Element → Any Element Documentation ownership, from the backstage.io/techdocs-entity annotation

Two long-standing relations are wider than they first look, because of how the descriptor format declares the underlying spec field: spec.system is declared on Component, API and Resource, so all three can sit in a system, and providesAPI/consumesAPI are declared on System as well as Component — a System is defined as exposing one or several public APIs, so a System source is ordinary Backstage rather than an edge case.

dependsOn is the general reading of spec.dependsOn and covers component-to-component dependencies as well as resource dependencies; usesResource is retained as its resource-facing subproperty for continuity with older data. A lift emitting fresh data should prefer dependsOn and let subproperty entailment cover usesResource queries.

Value Vocabularies

Backstage's spec.type fields and lifecycle/visibility states are modelled as named individuals (DD-23) — classes of individuals with no owl:oneOf, so an adopter can mint their own member in their own namespace without forking a published axiom:

Vocabulary Property Individuals (documented examples) Closure
bs:LifecycleState bs:lifecycleState bs:Experimental, bs:Production, bs:DeprecatedState Open (Warning if outside the set)
bs:ApiVisibility bs:apiVisibility bs:Public, bs:Restricted, bs:Private Closed (Violation if outside the set — fixed by the system model)
bs:ComponentType bs:componentType bs:ServiceType, bs:WebsiteType, bs:LibraryType Open
bs:APIType bs:apiType bs:OpenAPI, bs:AsyncAPI, bs:GraphQL, bs:GRPC Open
bs:ResourceType bs:resourceType bs:DatabaseResourceType, bs:S3BucketResourceType, bs:KubernetesClusterResourceType Open, illustrative only
bs:GroupType bs:groupType bs:TeamGroupType, bs:BusinessUnitGroupType, bs:ProductAreaGroupType, bs:RootGroupType Open, illustrative only
bs:SystemType bs:systemType bs:ProductSystemType, bs:ServiceSystemType, bs:FeatureSetSystemType Open, illustrative only
bs:DomainType bs:domainType bs:ProductAreaDomainType, bs:ProductGroupDomainType, bs:BundleDomainType Open, illustrative only

Each individual carries skos:notation with the exact YAML token (e.g. "openapi", "database") — that is the value a lift matches against when converting catalog-info.yaml to RDF.

Lifecycle

bs:lifecycle (a plain string) is deprecated as of 0.3.0. Use bs:lifecycleState, which points at a bs:LifecycleState individual — bs:Experimental, bs:Production, or bs:DeprecatedState — instead of a bare string. The old property still validates (with a migration Warning), but new models should use bs:lifecycleState from the start:

# Before (0.2.0, deprecated)
:payment-processor bs:lifecycle "production" .

# After (0.3.0)
:payment-processor bs:lifecycleState bs:Production .

Entity Identity

A Backstage entity is identified by its entity reference, kind:namespace/name. All four identity fields are retained source data (DD-24) — read from catalog-info.yaml, not computed:

Field Property Required by the shapes
kind bs:kind (a plain string, title-case: "Component", "API") No — maxCount 1 only
namespace bs:namespace (absent means "default") Recommended — Warning if absent
name bs:name Yes — Violation if absent
the whole reference bs:entityRef No — maxCount 1 only
:payment-processor
    a               bs:Component ;                              # how THIS GRAPH classifies it
    bs:kind         "Component" ;                                # what the CATALOGUE said
    bs:namespace    "default" ;
    bs:name         "payment-processor" ;
    bs:entityRef    "component:default/payment-processor" ;      # the reference, as the catalogue circulates it
    skos:prefLabel  "payment-processor"@en .                      # the human-readable label

bs:kind and rdf:type are both recorded, and may diverge

They answer different questions. bs:kind is what the descriptor said; rdf:type is how this graph classifies the entity, which is a modelling decision open to revision later. A lift maps one to the other, but an adopter may retype a lifted entity under a convention adopted afterwards, or carry additional types from another notation in a merged graph — and neither makes the retained kind wrong.

Keeping only rdf:type would lose the descriptor's own statement the moment either happened. Keeping only bs:kind would leave the entity invisible to every sh:targetClass and rdfs:subClassOf query in the repository. So both are kept, and neither is derived from the other.

The mapping between them is published on the classes, not hardcoded in each lift — each of the seven element classes carries skos:notation with its exact kind token:

bs:Component skos:notation "Component" .
bs:API       skos:notation "API" .

So a lift resolves kind: Componentbs:Component by matching skos:notation, exactly as it resolves type: openapibs:OpenAPI. These tokens are title-case, unlike every other skos:notation in the ontology, which carries a lowercase spec.type or state token.

Divergence is reported by bssh:KindTypeAlignmentShape at sh:Info — deliberately not an error. A mis-mapped lift and a deliberate retyping look identical in the data, so the shape makes both visible for a reconciliation pass rather than pronouncing on which one you meant. Switch it off where retyping is standing policy.

bs:entityRef is what the catalogue circulates

The entity reference is not a convenience we assemble — it is the form Backstage itself uses in the relations array and in spec.owner, spec.system, spec.dependsOn and the techdocs-entity annotation. Retaining it verbatim keeps resolution honest: the specification defines the default-namespace rule and case-insensitive comparison, and a consumer that rebuilds the reference reimplements those rules and may diverge from the catalogue. A reference the catalogue actually emitted is evidence; a reconstruction is an assumption. It also makes resolving an incoming reference one triple pattern instead of a three-way join with string concatenation.

bssh:EntityRefConsistencyShape checks it against bs:kind, bs:namespace and bs:name — the three other retained fields from the same descriptor, so a mismatch means the lift mangled one of them, which is a genuine Violation. It is not checked against rdf:type, because validating the reference against a type the modeller is free to change would report every intentionally retyped entity as broken.

Both bs:kind and bs:entityRef are targeted with sh:targetSubjectsOf, so a natively authored model that has no descriptor to retain simply omits them and is not reported as incomplete — as the hand-written PayFlow example does.

Two things not to do

bs:name does not substitute for skos:prefLabel — emit both. And bs:title is presentation-only; putting it in skos:prefLabel in place of the name makes two entities sharing a title indistinguishable in every label-driven view.

Do not use bs:uid as a join key. The catalog assigns it on first ingestion and the specification warns it changes when the identical file is unregistered and re-registered, so it identifies a registration rather than the thing. The entity reference is what joins across sources.

Semantic Assets

Asset Set

modelingLanguages/backstage/
├── backstage-metamodel.ttl             ← Entry point: arch:Metamodel
├── backstage-onto.ttl                  ← OWL ontology (7 elements, 12 relationships, 8 value vocabularies)
├── backstage-tax.ttl                   ← SKOS taxonomy
├── backstage-shapes.ttl                ← SHACL validation
├── backstage-viewpoints.ttl            ← 7 viewpoints
├── backstage-reference-data.ttl        ← Cross-module correspondences (lifecycle ↔ arch-processes stages)
├── backstage-presentation-contexts.ttl ← Developer, Architect, Stakeholder themes
└── backstage-deliverable-templates.ttl ← Document templates

The value vocabularies themselves — lifecycle, API visibility, component/API/resource/group/system/domain types — moved from backstage-reference-data.ttl into backstage-onto.ttl as of 0.3.0, as named individuals (DD-23), the same pattern UML uses for uml:MessageSort. backstage-reference-data.ttl now holds only what sits around those values — correspondences to other vocabularies (e.g. bs:Production skos:closeMatch ap:Active, aligning Backstage lifecycle to the notation-agnostic Architecture Processes extension) and any organisation-specific reference data an adopter adds. Two IRIs for one value was the failure mode this reconciliation removed.

Concept-to-Asset Mapping

Backstage Concept Semantic Representation File
Entity types (Component, System, etc.) owl:Class rdfs:subClassOf arch:Element backstage-onto.ttl
Relationships (ownedBy, providesAPI, dependsOn, childOf, etc.) Three-declaration pattern backstage-onto.ttl
Lifecycle, visibility, component/API/resource/group/system/domain types owl:NamedIndividual (DD-23, no owl:oneOf) backstage-onto.ttl
Cross-vocabulary correspondences (e.g. lifecycle ↔ arch-processes stages) skos:closeMatch backstage-reference-data.ttl
Validation (identity, naming, relationship domain/range, vocabulary closure) sh:NodeShape backstage-shapes.ttl
Viewpoints arch:Viewpoint backstage-viewpoints.ttl

Cross-Language Integration

Backstage entities map naturally to ArchiMate and C4:

Backstage ArchiMate 4.0 C4
bs:Component am4:ApplicationComponent c4:Container or c4:Component
bs:System (group of components) c4:SoftwareSystem
bs:API am4:ApplicationInterface
bs:Resource am4:Node / am4:Artifact c4:DeploymentNode
bs:Group am4:Role

Worked Example: PayFlow (Fintech Platform)

Prerequisites

core/core-onto.ttl
modelingLanguages/backstage/backstage-onto.ttl
modelingLanguages/backstage/backstage-tax.ttl
modelingLanguages/backstage/backstage-shapes.ttl
modelingLanguages/backstage/backstage-viewpoints.ttl
modelingLanguages/backstage/backstage-reference-data.ttl
modelingLanguages/backstage/backstage-metamodel.ttl

About PayFlow

PayFlow is a fintech platform providing payment processing, merchant onboarding, and financial reporting as a service. The engineering organization is structured into domain-aligned squads using Backstage as their developer portal.

The complete, validatable example model is available at examples/payflow/payflow-model.ttl.

Model File Header

Start every Backstage model with this header, which binds it to the Backstage metamodel:

@prefix skos:    <http://www.w3.org/2004/02/skos/core#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix arch:    <https://meta.linked.archi/core#> .
@prefix bs:      <https://meta.linked.archi/backstage/onto#> .
@prefix :        <https://meta.linked.archi/examples/payflow/> .

:PayFlowModel
    a                          arch:Model ;
    skos:prefLabel             "PayFlow — Backstage Catalog Model"@en ;
    arch:modelConformsToMetamodel <https://meta.linked.archi/backstage/metamodel#Backstage> ;
    dcterms:created            "2025-06-27"^^xsd:date ;
.

The model file also needs @prefix xsd: for the date literal above; the full header is in the example file.

Element Patterns

Paste these patterns to declare each catalogue entity with its identity, type, and label, then rename them to match your estate. Every entity carries bs:name (the catalog identity, required by BackstageEntityIdentityShape) alongside skos:prefLabel (the human-readable label):

## Domains
:PaymentsDomain a bs:Domain ; bs:name "payments" ; skos:prefLabel "Payments"@en .
:MerchantDomain a bs:Domain ; bs:name "merchant-services" ; skos:prefLabel "Merchant Services"@en .
:PlatformDomain a bs:Domain ; bs:name "platform" ; skos:prefLabel "Platform"@en .

## Groups (teams) — spec.type is optional but recommended
:PaymentsSquad a bs:Group ; bs:name "payments-squad" ; bs:groupType bs:TeamGroupType ; skos:prefLabel "Payments Squad"@en .
:MerchantSquad a bs:Group ; bs:name "merchant-squad" ; bs:groupType bs:TeamGroupType ; skos:prefLabel "Merchant Squad"@en .
:PlatformTeam a bs:Group ; bs:name "platform-team" ; bs:groupType bs:TeamGroupType ; skos:prefLabel "Platform Team"@en .
:SRETeam a bs:Group ; bs:name "sre-team" ; bs:groupType bs:TeamGroupType ; skos:prefLabel "SRE Team"@en .

## Group hierarchy — spec.parent, stored child-to-parent
:SRETeam bs:childOf :PlatformTeam .

## Systems
:PaymentGateway a bs:System ; bs:name "payment-gateway" ; skos:prefLabel "Payment Gateway"@en .
:MerchantPortal a bs:System ; bs:name "merchant-portal" ; skos:prefLabel "Merchant Portal"@en .
:ObservabilityPlatform a bs:System ; bs:name "observability-platform" ; skos:prefLabel "Observability Platform"@en .

## Components — bs:lifecycleState points at a bs:LifecycleState individual, not a string
:PaymentProcessor a bs:Component ; bs:name "payment-processor" ; skos:prefLabel "payment-processor"@en ; bs:lifecycleState bs:Production .
:FraudService a bs:Component ; bs:name "fraud-service" ; skos:prefLabel "fraud-service"@en ; bs:lifecycleState bs:Production .
:PayoutService a bs:Component ; bs:name "payout-service" ; skos:prefLabel "payout-service"@en ; bs:lifecycleState bs:Production .
:MerchantOnboarding a bs:Component ; bs:name "merchant-onboarding" ; skos:prefLabel "merchant-onboarding"@en ; bs:lifecycleState bs:Production .
:ReportingService a bs:Component ; bs:name "reporting-service" ; skos:prefLabel "reporting-service"@en ; bs:lifecycleState bs:Experimental .
:WebhookDispatcher a bs:Component ; bs:name "webhook-dispatcher" ; skos:prefLabel "webhook-dispatcher"@en ; bs:lifecycleState bs:Production .

## Component nesting — spec.subcomponentOf, a component packaged and shipped separately from its parent
:FraudModelScorer a bs:Component ; bs:name "fraud-model-scorer" ; skos:prefLabel "fraud-model-scorer"@en ;
    bs:lifecycleState bs:Experimental ; bs:subcomponentOf :FraudService .

## APIs — bs:apiType points at a bs:APIType individual
:PaymentsAPI a bs:API ; bs:name "payments-api" ; bs:apiType bs:OpenAPI ; skos:prefLabel "Payments API (OpenAPI)"@en .
:MerchantAPI a bs:API ; bs:name "merchant-api" ; bs:apiType bs:OpenAPI ; skos:prefLabel "Merchant API (OpenAPI)"@en .
:PaymentEventsAPI a bs:API ; bs:name "payment-events" ; bs:apiType bs:AsyncAPI ; skos:prefLabel "Payment Events (AsyncAPI)"@en .
:FraudScoringAPI a bs:API ; bs:name "fraud-scoring" ; bs:apiType bs:GRPC ; skos:prefLabel "Fraud Scoring API (gRPC)"@en .

## Resources — bs:resourceType is illustrative-only; mint your own individual where the
## documented examples (database, s3-bucket, kubernetes-cluster) don't fit
:PaymentsDB a bs:Resource ; bs:name "payments-db" ; bs:resourceType bs:DatabaseResourceType ; skos:prefLabel "payments-db (PostgreSQL)"@en .
:EventBus a bs:Resource ; bs:name "event-bus" ; bs:resourceType :MessageBrokerResourceType ; skos:prefLabel "event-bus (Kafka)"@en .
:MerchantDB a bs:Resource ; bs:name "merchant-db" ; bs:resourceType bs:DatabaseResourceType ; skos:prefLabel "merchant-db (PostgreSQL)"@en .
:RedisCache a bs:Resource ; bs:name "redis-cache" ; bs:resourceType :CacheResourceType ; skos:prefLabel "redis-cache"@en .

## Organisation-specific bs:ResourceType individuals — message-broker and cache have no
## documented example, so PayFlow mints them in its own namespace
:MessageBrokerResourceType a bs:ResourceType ; skos:prefLabel "Message Broker"@en ; skos:notation "message-broker" .
:CacheResourceType a bs:ResourceType ; skos:prefLabel "Cache"@en ; skos:notation "cache" .

Relationship Patterns

Wire the entities together with ownership, system membership, API dependencies, and general dependency:

## Ownership
:PaymentProcessor bs:ownedBy :PaymentsSquad .
:FraudService bs:ownedBy :PaymentsSquad .
:PayoutService bs:ownedBy :PaymentsSquad .
:MerchantOnboarding bs:ownedBy :MerchantSquad .
:ReportingService bs:ownedBy :MerchantSquad .
:WebhookDispatcher bs:ownedBy :PlatformTeam .
:PaymentGateway bs:ownedBy :PaymentsSquad .
:MerchantPortal bs:ownedBy :MerchantSquad .

## System membership
:PaymentProcessor bs:partOfSystem :PaymentGateway .
:FraudService bs:partOfSystem :PaymentGateway .
:PayoutService bs:partOfSystem :PaymentGateway .
:WebhookDispatcher bs:partOfSystem :PaymentGateway .
:MerchantOnboarding bs:partOfSystem :MerchantPortal .
:ReportingService bs:partOfSystem :MerchantPortal .

## API provision
:PaymentProcessor bs:providesAPI :PaymentsAPI .
:PaymentProcessor bs:providesAPI :PaymentEventsAPI .
:FraudService bs:providesAPI :FraudScoringAPI .
:MerchantOnboarding bs:providesAPI :MerchantAPI .

## API consumption
:PayoutService bs:consumesAPI :PaymentsAPI .
:MerchantOnboarding bs:consumesAPI :PaymentsAPI .
:WebhookDispatcher bs:consumesAPI :PaymentEventsAPI .
:PaymentProcessor bs:consumesAPI :FraudScoringAPI .

## General dependency — spec.dependsOn between two Components (the reading usesResource
## does not cover, since usesResource's target is always a Resource)
:ReportingService bs:dependsOn :PaymentProcessor .

## Resource usage — the resource-facing subproperty of dependsOn
:PaymentProcessor bs:usesResource :PaymentsDB .
:PaymentProcessor bs:usesResource :EventBus .
:PaymentProcessor bs:usesResource :RedisCache .
:FraudService bs:usesResource :RedisCache .
:MerchantOnboarding bs:usesResource :MerchantDB .
:WebhookDispatcher bs:usesResource :EventBus .

## Domain membership — spec.domain is declared on System only
:PaymentGateway bs:belongsToDomain :PaymentsDomain .
:MerchantPortal bs:belongsToDomain :MerchantDomain .
:ObservabilityPlatform bs:belongsToDomain :PlatformDomain .

Service Catalogue View

graph TD
    subgraph PaymentsDomain["Payments Domain"]
        subgraph PG["Payment Gateway (System)"]
            PP["payment-processor<br/>Production"]
            FS["fraud-service<br/>Production"]
            PS["payout-service<br/>Production"]
            WD["webhook-dispatcher<br/>Production"]
        end
    end

    subgraph MerchantDomain["Merchant Services Domain"]
        subgraph MP["Merchant Portal (System)"]
            MO["merchant-onboarding<br/>Production"]
            RS["reporting-service<br/>Experimental"]
        end
    end

    PP -->|"provides"| PAPI["Payments API"]
    PP -->|"provides"| PEAPI["Payment Events"]
    FS -->|"provides"| FSAPI["Fraud Scoring API"]
    MO -->|"provides"| MAPI["Merchant API"]

    PS -->|"consumes"| PAPI
    MO -->|"consumes"| PAPI
    WD -->|"consumes"| PEAPI
    PP -->|"consumes"| FSAPI

    PP -->|"uses"| PDB["payments-db"]
    PP -->|"uses"| EB["event-bus"]
    MO -->|"uses"| MDB["merchant-db"]

    style PaymentsDomain fill:#B3D9FF,stroke:#333,color:#000
    style MerchantDomain fill:#D4E6B5,stroke:#333,color:#000
    style PG fill:#E3F2FD,stroke:#333,color:#000
    style MP fill:#E8F5E9,stroke:#333,color:#000

Validate the Catalogue with SHACL

Run this to check the model against the core and Backstage shapes. The ontology is passed alongside the model as data, not only as a shapes document: the model's bs:resourceType/bs:groupType/etc. values are individuals declared in backstage-onto.ttl (bs:DatabaseResourceType, bs:TeamGroupType, ...), and the sh:class constraints on those properties need that typing present in the validated graph, not only in the shapes graph:

.scripts/validate.sh --shacl \
    "modelingLanguages/backstage/backstage-onto.ttl,examples/payflow/payflow-model.ttl" \
    core/core-shapes.ttl \
    modelingLanguages/backstage/backstage-shapes.ttl

PayFlow is the regression test for the 0.3.0 migration: every entity carries bs:name, every lifecycle uses bs:lifecycleState, and every spec.type value is a proper vocabulary individual (two of them, message-broker and cache, minted locally since Backstage's bs:ResourceType examples don't cover them) — it validates with zero Violations. The only findings are four sh:Warnings on bs:definition, one per API, since this EA-level model deliberately keeps the type (bs:apiType) and skips the raw OpenAPI/AsyncAPI/gRPC document body — exactly the case ApiDefinitionShape's comment anticipates.

Further Reading

Comparison Articles

For detailed comparisons of Backstage with other modelling languages in Linked.Archi:


Disclaimer: This is a community semantic representation of the Backstage software catalogue model for interoperability purposes. It is not produced by, endorsed by, or affiliated with Spotify or the Backstage project. "Backstage" is a trademark of Spotify AB.