- What it is: an internal developer portal (IDP) is the self-service surface a platform team uses to expose infrastructure to application developers. It bundles a software catalog, a set of golden-path templates, and a scaffolder behind one front door, so developers ask for a capability instead of assembling it.
- Why it matters: it moves cognitive load off product teams. Instead of each developer learning the cluster, the CI system, the secrets manager, and the observability stack, they ask the portal for a service and get a paved road that already wires those pieces together.
- The gotcha: shelfware. A portal developers quietly route around, or a catalog that goes stale within weeks of launch, is worse than no portal at all, because it manufactures the appearance of governance without the substance.
Who this is for: platform engineers and engineering leaders standing up a platform team or an internal developer portal for the first time. The assumption is that you already run on Kubernetes and have a working CI system, but you do not yet have a self-service layer on top of them. Developers still file tickets or copy-paste a neighboring service to start something new. If that is you, this is the layer that turns “ask the platform team” into “ask the portal.”
The hidden tax on every new service
Walk into most engineering organizations that have grown past a handful of teams and ask how a developer spins up a new service. The honest answer is usually a wince. They clone the repository that looks closest to what they need, delete the parts that do not apply, hunt down whichever Slack thread explains how to register a CI pipeline, ask in the platform channel what the current logging conventions are, and three days later they have something that deploys. Multiply that by every new service, every new hire who has to relearn the ritual, and every drift between what the copied template did six months ago and what it should do today.
That friction is a tax, and it is paid in the most expensive currency an organization has: senior engineering attention spent on undifferentiated setup. Platform engineering is the discipline of paying that tax down by treating the internal platform as a product, with the development teams as its customers. The internal developer portal is where that product shows its face. It is not the platform itself, the clusters, pipelines, and cloud accounts are the platform, but it is the surface through which developers consume the platform without having to understand its internals.
The framing that has carried platform engineering into the mainstream is the one from Team Topologies [1]: a platform team’s job is to reduce the cognitive load on stream-aligned teams by offering self-service capabilities as a compelling internal product. “Compelling” is the load-bearing word. Nobody is forced to use an internal platform the way they are forced to use, say, the company’s payroll system. If the paved road is slower or more confusing than going around it, developers go around it, and you have built shelfware. The CNCF’s platforms working group makes the same point in its Platforms white paper [2]: the value of a platform is measured by adoption and the load it removes, not by the surface area of features it ships.
By 2026 the IDP space has settled into a few recognizable options. Backstage [3], originally from Spotify and now a CNCF project, is the open-source build-it-yourself standard. Port and Cortex are the established SaaS entrants that trade some control for a faster start. Humanitec comes at it orchestration-first, leading with the deployment layer rather than the catalog. They differ mostly on a single axis: how much the platform team owns and operates versus how much individual service teams self-manage. We will come back to that trade in the related-work section. The mechanics that follow, catalog, templates, and adoption measurement, matter regardless of which one you pick.
What an internal developer portal actually contains
Strip away the marketing and an IDP is three things wired together, plus the platform underneath them.
flowchart LR
Dev[developer] --> Portal[internal developer portal]
Portal --> Cat[software catalog]
Portal --> Scaf[scaffolder / templates]
Portal --> CD[CI/CD]
Portal --> Infra[cloud / infra]
Repos[(service repos<br/>source of truth)] -. ingested .-> Cat
Scaf --> Repos
Scaf --> CD
Scaf --> Infra
The software catalog is the inventory: every service, library, and resource, who owns it, what it depends on, where its runbook lives. The scaffolder is the factory: the templates that stamp out new services. The CI/CD and infra wiring is what those templates reach into when they provision something real. And the arrow that makes or breaks the whole thing is the dotted one from the service repos into the catalog. The repositories are the source of truth, and the catalog is a projection of them. Get that arrow backwards, let the catalog be the place where ownership and dependency data is authored, and you have built the staleness problem into the foundation. The next section is entirely about getting that arrow right.
Service catalog design: source from the repo, enforce in CI
A service catalog is only as useful as developers trust it to be. The first time someone looks up the owner of a failing service, pages the name in the catalog, and gets a person who left the company eight months ago, they stop trusting the catalog. After that it is decoration.
The most common way catalogs rot is depressingly predictable. The portal ships with a nice UI for editing service metadata, ownership, lifecycle, dependency links, runbook URLs, so the initial population happens by hand in that UI. It looks great at launch. Then a team reorganizes, a service changes hands, a dependency is added, and nobody opens the portal to update the entry, because updating the portal is a separate chore disconnected from the work that caused the change. Within a quarter the catalog describes an organization that no longer exists.
The fix is to invert the authorship. Catalog metadata should live in the repository it describes, as a descriptor file the portal ingests on a schedule, rather than in the portal’s own database. Backstage formalizes this with catalog-info.yaml [4]: each service carries a descriptor at its repo root, Backstage discovers and re-ingests it periodically, and the catalog becomes a read-only projection of what is in version control. Now updating ownership is a pull request against the service, reviewed by the people who own it, landing next to the code change that motivated it. The metadata moves at the speed of the code because it is part of the code.
Here is a realistic descriptor for a payments service. It declares the component, its owner, its lifecycle, the API it provides, and what it consumes:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-api
description: Handles charge capture and refund settlement for storefront checkout.
annotations:
github.com/project-slug: acme/payments-api
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: PXYZ123
tags:
- go
- tier-1
links:
- url: https://runbooks.internal/payments-api
title: On-call runbook
icon: docs
spec:
type: service
lifecycle: production
owner: team-payments
system: checkout
providesApis:
- payments-api
consumesApis:
- ledger-api
dependsOn:
- resource:payments-postgres
The owner is a team reference, not a person, so it survives individual departures. The dependsOn and consumesApis fields are what let the catalog draw a real dependency graph, which is the thing that turns it from a phone book into something you actually reach for during an incident. The annotations are how Backstage links the entry to external systems: the GitHub slug for source, the PagerDuty service for on-call, the TechDocs reference for documentation. Each annotation is a join key into a tool the team already uses, which is the point: the catalog stitches existing systems together rather than asking anyone to re-enter data.
Sourcing from the repo solves authorship but not coverage. A descriptor that does not exist cannot go stale, but it also cannot be ingested, and a service with no catalog entry is invisible to the portal. So the second half of the pattern is enforcement: a CI check that fails the pipeline when catalog-info.yaml is missing or malformed. This is a lightweight governance mechanism that scales without a dedicated governance team, because the enforcement is mechanical and lives where the work happens.
name: catalog-info-check
on:
pull_request:
branches: [main]
jobs:
validate-catalog-info:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail if catalog-info.yaml is missing
run: |
if [ ! -f catalog-info.yaml ]; then
echo "::error::catalog-info.yaml not found at repo root."
echo "Every service must declare a Backstage descriptor. See platform docs."
exit 1
fi
- name: Validate YAML and required fields
run: |
pip install yq >/dev/null 2>&1
for field in 'apiVersion' 'kind' '.metadata.name' '.spec.owner' '.spec.lifecycle'; do
value=$(yq -r "$field // empty" catalog-info.yaml)
if [ -z "$value" ]; then
echo "::error::catalog-info.yaml missing required field: $field"
exit 1
fi
done
echo "catalog-info.yaml is present and well-formed."
The check is deliberately blunt. It confirms the file exists, parses as YAML, and carries the handful of fields that make an entry useful: a kind, a name, an owner, and a lifecycle. You can grow it later into full schema validation against the Backstage entity spec, but even this minimal version closes the coverage gap, because a service literally cannot merge to main without a descriptor. The freshness comes from repo-sourced authorship; the coverage comes from the gate. You need both. Automation that keeps existing entries fresh does nothing about the service that never got an entry, and a gate that demands a file does nothing about the file that was correct at creation and rotted afterward.
Golden-path templates: a living, versioned API
If the catalog is the inventory, golden-path templates are the factory floor, and they are the highest-leverage thing a platform team ships. A golden path is a self-service workflow that provisions a fully configured new service, the repository, the CI pipeline, observability wiring, and a passing initial test suite, in well under 10 minutes instead of three days.
flowchart LR
Req[developer requests<br/>new service] --> Scaf[scaffolder runs template]
Scaf --> Repo[repo created<br/>+ catalog-info.yaml]
Scaf --> CI[CI pipeline wired]
Scaf --> Obs[dashboards + alerts<br/>provisioned]
Repo --> Reg[registered in catalog]
CI --> Deploy[first deploy]
Obs --> Deploy
Deploy --> Done[running service<br/>in under 10 min]
Backstage implements this with Software Templates [5], and Port offers the same shape through its self-service actions. A template declares a set of parameters it asks the developer for, then a sequence of steps it runs to turn those answers into real artifacts. Here is the skeleton of one that scaffolds a Go service, publishes the repository, registers it in the catalog, and hands off to the infra layer:
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: go-service
title: Go microservice
description: Scaffolds a production-ready Go service with CI, observability, and a passing test.
spec:
owner: team-platform
type: service
parameters:
- title: Service details
required: [name, owner, system]
properties:
name:
title: Service name
type: string
pattern: '^[a-z][a-z0-9-]+$'
owner:
title: Owning team
type: string
ui:field: OwnerPicker
system:
title: System
type: string
steps:
- id: fetch-base
name: Fetch skeleton
action: fetch:template
input:
url: ./skeleton
values:
name: $
owner: $
system: $
- id: publish
name: Publish repository
action: publish:github
input:
repoUrl: github.com?owner=acme&repo=$
defaultBranch: main
protectDefaultBranch: true
- id: register
name: Register in catalog
action: catalog:register
input:
repoContentsUrl: $
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Open repository
url: $
- title: Open in catalog
entityRef: $
The skeleton directory the fetch:template step pulls from is where the opinions live: a catalog-info.yaml pre-filled with the parameters, a CI workflow file, a Dockerfile, a health-check endpoint, a starter test, and the dashboard and alert definitions the observability step provisions. The template is not a wizard that produces a blank repo. It produces a repo that is already a first-class citizen of the platform, registered in the catalog and deployable from minute one.
The critical idea, and the second of this article’s three, is that the template is a living, versioned artifact and you should treat its breaking changes like an API contract. A template encodes the platform team’s current opinions on language versions, build tools, secret management, base images, and deployment targets. Those opinions change. When you bump the base image, switch the secrets backend, or restructure the CI workflow, every service ever scaffolded from that template now diverges from the current paved road. If you ship that change silently, you fragment your fleet into archaeological layers, each cohort of services carrying the conventions that happened to be current the week it was created.
The discipline that prevents this is to version the template and communicate breaking changes the way you would for any consumed API. Semantic versioning on the template, a changelog that calls out what a bump requires service owners to do, and a deprecation window before you retire an old major version. The point is not bureaucracy. It is that the service teams downstream of your template are your API consumers whether you acknowledge it or not, and the difference between a platform team that is trusted and one that is feared is whether changes to the paved road arrive as announced upgrades or as surprise breakage.
For the infra-provisioning layer that a template’s steps reach into, two patterns dominate in 2026. Score [6] is a workload specification that lets a developer declare what their service needs, a Postgres database, a queue, an environment variable, in a platform-agnostic file that a separate tool translates into concrete infrastructure. A score.yaml stays small and intention-revealing:
apiVersion: score.dev/v1b1
metadata:
name: payments-api
containers:
payments-api:
image: registry.internal/payments-api:latest
variables:
DATABASE_URL: ${resources.db.connection}
resources:
db:
type: postgres
Crossplane [7] sits a layer lower, exposing cloud infrastructure as Kubernetes custom resources so the platform team can publish a curated, opinionated “database” or “bucket” abstraction that developers request without touching raw cloud APIs. Score answers “what does this workload need” in developer terms; Crossplane answers “how does the platform satisfy that need” in operator terms. A golden-path template that emits a score.yaml and lets a Crossplane-backed control plane realize it is the cleanest division of labor: the developer declares intent, the platform owns the mechanism.
Measuring adoption: the only number that matters is whether they use it
A portal that developers route around is worse than no portal, because it creates the illusion of governance without the substance. Leadership sees a catalog and assumes the organization is mapped; meanwhile the real service inventory lives in people’s heads and the catalog describes a fiction. So the third core idea is that you must measure adoption honestly, and a portal that is quietly being bypassed is shelfware no matter how polished it looks.
flowchart TB
Known[all known services] --> Fresh[have a fresh<br/>catalog entry]
Fresh --> Boot[bootstrapped via<br/>golden-path template]
Boot --> Active[actively used<br/>for day-to-day ops]
Known -. the gap is shelfware risk .-> Fresh
Three signals are worth tracking, and all three are ratios or trends rather than vanity totals.
The catalog freshness ratio is the number of services with an up-to-date catalog entry over the total number of services you know to exist. The denominator is the hard part and the honest part: it forces you to reconcile the catalog against an independent source of truth, the list of repositories, the set of running namespaces, the cloud resources actually billing. A freshness ratio computed only against services already in the catalog is circular and will always look good. Computed against reality, it tells you how much of your estate the portal can actually see.
The template bootstrap rate is how many new services are created through golden-path templates versus stood up by hand. This is the clearest signal of whether the paved road is genuinely faster than going around it. If developers keep hand-rolling services despite the template existing, the template is missing something they need, and that gap is your highest-priority backlog item. A rising bootstrap rate means the road is winning on its merits.
The time from intent to first deploy, measured before and after the portal, captures the headline promise: did self-service actually compress the path from “I need a service” to “it is running”? Track it as a trend, because the absolute number depends on your environment, but the direction is unambiguous and is exactly what justifies the platform team’s existence to anyone holding the budget.
Surfacing these in a recurring platform review keeps the team honest about whether the portal is reducing friction or accumulating it, and it converts arguments about what to build next into a data conversation. A low freshness ratio points you at catalog integration work; a low bootstrap rate points you at template gaps. The metrics are not a report card you file away. They are the steering wheel.
Related work and positioning
The IDP landscape sorts cleanly along the build-versus-buy axis, which is really an axis of how much control you want against how much operating burden you are willing to carry.
Backstage [3] is the open-source, build-it-yourself end. You run it, you extend it with plugins, you own the upgrades, and in return you get unlimited control and no per-seat bill. It is the right call for organizations large enough to staff a team that treats the portal itself as a product, and the wrong call for a small platform team that would spend its entire budget keeping Backstage’s plugin ecosystem patched instead of building paved roads. The gravitational pull of Backstage is strong because it is the CNCF reference implementation and the catalog format others interoperate with, but “free to license” and “free to operate” are very different numbers.
Port and Cortex are the SaaS end. You get a catalog, scaffolding, and self-service actions without running the software, which means a platform team can show value in weeks rather than quarters. The trade is the usual SaaS trade: less control over the data model and extension surface, a per-seat or per-service cost that grows with you, and your developer metadata living in a vendor’s system. For a team whose differentiator is its paved roads rather than its portal plumbing, that is often a good trade. For a team with unusual governance or data-residency constraints, the loss of control bites.
Humanitec approaches from a different starting point, leading with deployment orchestration and the platform-orchestrator pattern rather than the catalog. Where Backstage starts with “what services do we have,” Humanitec starts with “how does a service get deployed,” and the catalog is downstream of that. It pairs naturally with the Score specification [6] it helped popularize. Which framing fits depends on whether your acute pain is discovery and ownership, which pulls you toward catalog-first tools, or provisioning and deployment, which pulls you toward orchestration-first ones.
None of these is a substitute for the discipline underneath. A portal is a multiplier on a platform-as-a-product practice [1][2]. Buy or build the surface, but the catalog still goes stale if you author it in the UI, the templates still fragment your fleet if you version them carelessly, and the whole thing is still shelfware if you do not measure adoption. The tool choice is the least consequential decision in this article.
Where it breaks
Internal developer portals fail in recognizable ways, and knowing the failure modes up front is most of avoiding them.
Shelfware from a weak value proposition. The deepest failure is building a portal nobody chooses to use because going around it is faster. This happens when the portal is launched as a mandate rather than a product, when the golden path is slower or more constrained than the copy-paste ritual it replaces, or when it was built to satisfy a leadership desire for a single pane of glass rather than to solve a developer’s actual problem. The bootstrap-rate metric is your early warning, and the fix is always the same: make the paved road genuinely the path of least resistance, then it sells itself.
Catalog staleness despite automation. Sourcing from the repo and gating in CI dramatically reduces rot, but it does not eliminate it. Descriptors drift when the required fields are too few to be meaningful, when teams game the CI check by committing a minimal file they never maintain, or when the ingestion schedule is slow enough that the catalog lags reality by days. Repo-sourcing moves the staleness problem; it does not delete it. You still have to make the descriptor genuinely useful enough that keeping it accurate is in the owning team’s own interest.
Template sprawl and versioning debt. The flip side of golden paths being easy to create is that they proliferate. A team that ships a new template for every slight variation ends up with 40 near-identical templates, none of them quite maintained, and the “golden” path is now a maze. Worse, if templates are not versioned with real deprecation discipline, every old major version still in the wild is a maintenance liability and a security exposure. The fix is to treat templates as a small, curated, versioned library, the same restraint the Argo article makes about analysis templates, not an unbounded pile.
The platform team as bottleneck. The entire premise is self-service, so any design that funnels requests back through the platform team for approval or manual action quietly recreates the ticket queue the portal was supposed to kill. If provisioning a database still requires a platform engineer to click something, you have built a prettier ticketing system. Self-service has to mean self-service all the way through, which is exactly why the Crossplane-and-Score division of labor matters: it lets the platform encode its guardrails once so the developer never has to wait on a human.
Over-abstraction that hides too much. The opposite failure of being a bottleneck is abstracting so aggressively that the platform becomes a black box. Senior developers who need to debug a production incident, tune a resource limit, or understand why a deploy behaved oddly hit a wall when the platform has hidden every underlying primitive behind a friendly form. The best paved roads are paved, not walled: they make the easy thing trivial without making the necessary escape hatch impossible. When the abstraction leaks, and it always eventually does, the developer needs a way down to the layer beneath.
Further reading
Start with the Team Topologies framing of the platform as a product [1] and the CNCF Platforms white paper [2], which together establish why adoption and cognitive load, not feature count, are the measures that matter. For the catalog, the Backstage software catalog overview [3] and the catalog-info.yaml descriptor format reference [4] are the canonical sources for the repo-sourcing pattern. The Backstage Software Templates documentation [5] covers the scaffolder parameters and steps the golden-path section is built on. For the infra-provisioning layer, the Score specification [6] and the Crossplane documentation [7] explain the intent-versus-mechanism division of labor. To compare the buy options against build, the Port [8] and Cortex [9] documentation lay out the SaaS model, and the platformengineering.org community resources [10] are the best running source for current practice across the discipline.
Written with AI assistance, editorially reviewed.
References
[1] M. Skelton and M. Pais, “Team Topologies: Platform as a Product.” https://teamtopologies.com/key-concepts
[2] CNCF App Delivery TAG, “Platforms White Paper.” https://tag-app-delivery.cncf.io/whitepapers/platforms/
[3] Backstage, “Software Catalog Overview.” https://backstage.io/docs/features/software-catalog/
[4] Backstage, “Descriptor Format of Catalog Entities (catalog-info.yaml).” https://backstage.io/docs/features/software-catalog/descriptor-format/
[5] Backstage, “Backstage Software Templates.” https://backstage.io/docs/features/software-templates/
[6] Score, “Score Specification.” https://docs.score.dev/docs/
[7] Crossplane, “Crossplane Documentation.” https://docs.crossplane.io/latest/
[8] Port, “Port Documentation.” https://docs.port.io/
[9] Cortex, “Cortex Documentation.” https://docs.cortex.io/
[10] Platform Engineering, “Platform Engineering Community and Resources.” https://platformengineering.org/
[11] CNCF, “Backstage Project.” https://www.cncf.io/projects/backstage/
[12] Humanitec, “Platform Orchestrator.” https://developer.humanitec.com/