2026-07-30 · 9 min read
What an Internal Developer Platform Actually Looks Like
Beyond the buzzword, a working reference for an Internal Developer Platform: Backstage service catalog, golden-path scaffolder, and the paved-road defaults that tie GitOps, observability, and CI/CD together.

"We're building an Internal Developer Platform" has become the thing every infrastructure team says to justify their next quarter's roadmap. But when you ask what's actually in the platform, answers get vague fast. Backstage? A portal? Kubernetes with extra steps?
I built a reference IDP to answer that question concretely. Not a demo, a working scaffold that wires together the pieces a platform team actually ships: a service catalog with ownership, a golden-path scaffolder that produces fully operational services, and paved-road defaults connecting GitOps, observability, CI/CD, and secrets management. Here's what it looks like in practice.
Platform Engineering Is Not a Tool
Platform engineering is a discipline, not a product. The goal: reduce cognitive load for product teams by curating opinionated, self-service infrastructure behind clean abstractions. Backstage is a portal, a UI layer. The platform is everything behind it: the templates, the automation, the contracts between teams and infrastructure.
A useful mental model:
| Layer | What it does | Example |
|---|---|---|
| Portal | Discovery, scaffolding, docs | Backstage |
| Golden paths | Opinionated service templates | Scaffolder + cookiecutter |
| Paved roads | Shared infra with sane defaults | GitOps repos, CI templates, monitoring |
| Foundation | Kubernetes, networking, IAM | EKS/GKE + Terraform |
My reference repo (platform-engineering-idp) covers the top three layers. The foundation layer is handled by a separate Terraform repo, this IDP assumes a running cluster exists.
The Service Catalog: Who Owns What
The catalog is the single source of truth for services, their owners, and their dependencies. Every repo contains a catalog-info.yaml that Backstage discovers:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-service
description: Processes payment intents and webhook events
annotations:
github.com/project-slug: durrello/payment-service
backstage.io/techdocs-ref: dir:.
tags:
- golang
- grpc
spec:
type: service
lifecycle: production
owner: team-payments
system: checkout-system
providesApis:
- payment-api
dependsOn:
- resource:postgres-payments
- component:notification-service
This isn't just metadata for a pretty UI. It answers real questions at 2 AM: who owns this service? What does it depend on? Where are the docs? The system field groups related components so you can reason about boundaries, not just individual microservices.
I define Systems and APIs as first-class entities too:
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: checkout-system
description: End-to-end purchase flow
spec:
owner: team-payments
domain: commerce
Ownership is enforced, every Component must declare an owner that maps to a Team entity. No orphan services.
The Golden-Path Scaffolder
This is where the platform becomes self-service. A developer fills out a form in Backstage (service name, team, language, needs a database?) and the scaffolder generates a complete, production-ready repository.
What "production-ready" means in this context:
- Application code: Go or Node.js skeleton with health checks, graceful shutdown, structured logging
- Dockerfile: multi-stage, distroless base, non-root user
- CI pipeline: GitHub Actions workflow referencing shared templates from
cicd-pipeline-templates - Kubernetes manifests: Deployment, Service, HPA, PodDisruptionBudget in a
deploy/directory - GitOps registration: an ApplicationSet generator entry so Argo CD picks up the new service automatically
- Monitoring: ServiceMonitor + Grafana dashboard JSON provisioned via the
observability-stackpatterns - Secrets: Vault AppRole + ExternalSecret manifest pulling from
vault-on-kubernetespaths - catalog-info.yaml: pre-filled with the team's ownership
The scaffolder template (templates/golden-path-service/template.yaml) uses Backstage's fetch:template and publish:github actions:
steps:
- id: fetch
name: Fetch skeleton
action: fetch:template
input:
url: ./skeleton
values:
serviceName: ${{ parameters.serviceName }}
owner: ${{ parameters.owner }}
language: ${{ parameters.language }}
needsDatabase: ${{ parameters.needsDatabase }}
- id: publish
name: Create GitHub repo
action: publish:github
input:
repoUrl: github.com?owner=durrello&repo=${{ parameters.serviceName }}
defaultBranch: main
protectDefaultBranch: true
- id: register
name: Register in catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
A developer goes from "I need a new service" to a deployed, monitored, owned service in under 10 minutes. No Jira ticket to the platform team.
The Paved-Road Philosophy
Golden paths are opinionated by design. But "opinionated" doesn't mean "locked in." The philosophy:
Opinionated defaults: Every generated service ships with structured JSON logging, Prometheus metrics on :9090/metrics, readiness/liveness probes, and a standard CI pipeline. You get these without asking.
Escape hatches: Need a custom build step? Override the CI template's matrix. Need a non-standard port? Change the Helm values. The platform provides guardrails, not walls.
Consistency over flexibility: If 40 services use the same logging format, your observability stack works out of the box. One team's "creative" log format is everyone's on-call nightmare.
The defaults are encoded in shared repos that the IDP references:
| Concern | Repo | What it provides |
|---|---|---|
| Deployment | gitops-kubernetes-platform | Argo CD ApplicationSets, namespace conventions, RBAC |
| Monitoring | observability-stack | Prometheus + Grafana + Loki stack, shared dashboards |
| CI/CD | cicd-pipeline-templates | Reusable GitHub Actions workflows (build, scan, deploy) |
| Secrets | vault-on-kubernetes | Vault config, AppRole policies, ExternalSecrets operator |
Each repo is independently versioned. The IDP pins to specific versions so a platform upgrade doesn't silently break 50 services.
How It All Ties Together
Here's the flow when a developer scaffolds a new service:
- Backstage scaffolder creates the repo from the golden-path template
- The repo includes a GitHub Actions workflow that imports shared steps from
cicd-pipeline-templates, build, container scan, push to registry - An Argo CD ApplicationSet in
gitops-kubernetes-platformuses a git-generator to discover the newdeploy/directory and syncs it to the cluster - The Kubernetes manifests include a ServiceMonitor: Prometheus (from
observability-stack) scrapes it automatically - An ExternalSecret pulls credentials from Vault paths defined in
vault-on-kubernetes - The catalog-info.yaml registers the service in Backstage, ownership, APIs, dependencies all visible immediately
No glue scripts. No manual steps. Each piece does one thing and connects through well-defined interfaces (Git repos, Kubernetes CRDs, Backstage entities).
Key Lessons
Start with the catalog, not the scaffolder. You can't build golden paths if you don't know what you already have. Register existing services first: even manually. Ownership alone is worth the effort.
The platform is the sum of its repos, not a monolith. Keeping GitOps config, CI templates, observability, and secrets in separate repos lets teams adopt incrementally. You don't need to buy the whole platform on day one.
Measure adoption, not coverage. A golden path nobody uses is shelf-ware. Track how many new services use the scaffolder vs. how many are created manually. If teams route around your platform, the platform is wrong.
Treat the platform as a product. It has users (developers), it needs feedback loops, and it competes with "just do it myself." If self-service is slower than a Slack message to your team, you've failed.
The full reference, catalog entities, scaffolder templates, skeleton code, and documentation on how each piece connects, is open source:
GitHub: github.com/durrello/platform-engineering-idp
Fork it, strip out what doesn't fit, and adapt the golden paths to your stack. The structure matters more than the specific tools.