Back
Building an Internal Developer Platform

Backstage, Golden Paths, and Self-Service Infrastructure

Author: Steven Crickman

Editor: Vahe Aslanyan

1. Introduction: Why Internal Developer Platforms Matter

As organizations scale past a handful of engineering teams, the cognitive load placed on individual developers grows faster than headcount. Provisioning a database, standing up a new service, configuring CI/CD, wiring up observability, and navigating security review all require context that no single engineer can hold for long. A developer who joined two years ago and shipped five services already knows the unwritten rules; a developer who joined two months ago is left guessing, asking around in Slack, or copy-pasting from whichever existing service looks closest to what they need — inheriting its mistakes along with its structure.

The traditional organizational response to this is a dedicated platform or DevOps team that handles requests via tickets: "please provision a database," "please set up a new CI pipeline," "please grant access to this bucket." This doesn't scale either — it just moves the bottleneck from "every developer needs infra knowledge" to "every infra request needs a human in the loop." As the org grows, the queue grows, and the platform team becomes the thing slowing everyone down, which is precisely the opposite of what platform teams are meant to do.

An Internal Developer Platform (IDP) is the architectural and organizational answer to this problem. It is a curated, self-service layer that sits between raw infrastructure (Kubernetes, cloud accounts, CI systems) and the application developers who need to consume it — without requiring those developers to become infrastructure experts themselves.

A good IDP does three things simultaneously:

  1. Reduces cognitive load by paving "golden paths" — opinionated, well-supported ways of doing common tasks, so a developer doesn't need to make a dozen infrastructure decisions just to ship a feature.
  2. Preserves autonomy by keeping the platform self-service rather than ticket-driven, so teams aren't blocked waiting on another team's queue.
  3. Maintains guardrails so that speed doesn't come at the cost of security, cost control, or operational consistency — the platform enforces policy structurally rather than through after-the-fact audits.

This handbook walks through building such a platform using Backstage as the developer portal layer, golden path templates as the paving mechanism, and a self-service infrastructure layer underneath, supported by policy-as-code guardrails and a product-oriented operating model for the platform team itself.

2. The Anatomy of a Platform

Before touching tooling, it's worth being precise about the layers involved, because conflating them is one of the most common causes of failed platform initiatives — teams often start by buying or building a portal UI, discover six months later that there's no underlying self-service automation behind it, and end up with a pretty catalog that still requires tickets for anything useful.

  • Infrastructure layer: Cloud accounts, Kubernetes clusters, networking, IAM, observability backends. Owned by a small SRE/infra team and typically pre-existing before any IDP work begins.
  • Platform layer: Abstractions over infrastructure — Terraform modules, Crossplane compositions, Helm charts, CI/CD pipeline definitions, policy engines. This is where "self-service infra" actually lives, and it's largely invisible to end users.
  • Experience layer: The interface developers actually touch — a portal, a CLI, an IDE plugin, or a chat-based interface. This is where Backstage lives, and it's the layer most people think of when they hear "developer platform," even though it's often the smallest piece of engineering effort relative to the platform layer underneath it.
  • Golden paths: Opinionated workflows that stitch the platform layer into ready-to-use templates, exposed through the experience layer. A golden path is the connective tissue between "infrastructure exists" and "a developer can use it without help."

A platform team's job is not to build infrastructure — that's typically already there, owned by SRE or cloud infra — but to build the glue that makes infrastructure consumable without a ticket queue. Treating the IDP as a software product, with developers as customers, is the single most important mental shift platform teams need to make, and it's a theme this handbook returns to repeatedly because it explains most of the difference between IDP efforts that stick and ones that quietly die after the initial launch enthusiasm fades.

It's also worth naming what an IDP is not. It is not a replacement for a service mesh, not a replacement for your CI system, and not a single tool you can buy off the shelf and be done with. It's an integration layer — its value comes from how well it stitches together things that already exist, plus a handful of opinionated defaults that didn't exist before.

3. Backstage: The Developer Portal Layer

3.1 What Backstage Is and Isn't

Backstage, originally built at Spotify and donated to the CNCF, is an open-source framework for building developer portals. It is not a finished product — it's a framework with a plugin architecture, a software catalog, and a templating engine (Scaffolder) that you assemble into your own portal. This distinction matters because organizations sometimes adopt Backstage expecting an out-of-the-box experience comparable to a SaaS product, then are surprised by the engineering investment required to make it genuinely useful. Budget for a small, dedicated team (often 2-4 engineers to start) who treat Backstage as a product they own, not a tool they install once.

Core components:

  • Software Catalog: A central, queryable inventory of every service, library, website, and resource in the org, described via YAML (catalog-info.yaml) files that live alongside the code they describe.
  • Scaffolder: A templating engine for golden path workflows — "create a new service" being the canonical example, though it generalizes to any multi-step, parameterized workflow.
  • TechDocs: Docs-as-code, rendering Markdown from source repos into a unified, searchable docs site, so documentation lives next to code instead of rotting in a separate wiki.
  • Plugins: Frontend and backend extensions (CI status, cost data, on-call info, security scan results) that surface into the catalog UI, making Backstage the aggregation point for information that otherwise lives scattered across a dozen different tools.

3.2 Deployment Considerations

Backstage ships as a Node.js application you deploy yourself — typically on Kubernetes, fronted by an ingress and backed by PostgreSQL. Key early decisions, each of which is expensive to reverse later:

  • Single instance vs. multi-tenant: Most orgs run one Backstage instance for the whole company; per-team instances fragment the catalog and defeat the central premise — a unified view of "everything that exists." If business units have genuinely separate compliance boundaries (e.g., a regulated subsidiary), consider catalog-level partitioning with shared infrastructure rather than wholly separate deployments.
  • Authentication: Integrate with existing SSO (Okta, Azure AD, GitHub) from day one. Auth bolted on after launch is painful because user identity threads through ownership records, RBAC policies, and audit logs — retrofitting it means touching nearly everything.
  • Database: PostgreSQL is required for production; SQLite is dev-only and will not survive real usage.
  • Ingestion strategy: Decide whether catalog entities are registered manually, auto-discovered from repos (via annotations in each repo's catalog-info.yaml combined with a discovery processor that scans your source control org), or synced from an external system of record (e.g., a CMDB or existing service registry). Auto-discovery scales better than manual registration but requires more upfront tooling work.
  • Hosting and scaling: For most mid-sized orgs, a modest Kubernetes deployment (2-3 replicas behind a load balancer, with the database as the actual scaling constraint) is sufficient. Treat Backstage's own uptime seriously — if the portal is down, golden paths stop working, which quickly becomes a visible, org-wide outage of developer productivity.

3.3 The Software Catalog as Source of Truth

The catalog models the organization as a graph of entities: Component, System, API, Resource, Group, User, Domain. Each Component (typically a service) declares ownership, its system membership, dependencies, and links to docs, dashboards, and runbooks.

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
 name: payments-api
 description: Handles payment authorization and settlement
 annotations:
   github.com/project-slug: org/payments-api
   backstage.io/techdocs-ref: dir:.
   pagerduty.com/service-id: PXXXXX
 tags:
   - payments
   - tier-1
spec:
 type: service
 lifecycle: production
 owner: team-payments
 system: payments
 providesApis:
   - payments-api-v2
 dependsOn:
   - resource:payments-db
   - component:fraud-detection

The catalog's value compounds over time: once every service declares its owner, you get instant answers to questions that previously required Slack archaeology — "who owns this," "what depends on this," "is this service deprecated," "what's the blast radius if this goes down." This is the difference between a catalog that's a nice-to-have directory and one that's load-bearing infrastructure for incident response and architectural decision-making.

Common failure mode: treating catalog population as a one-time migration project rather than an ongoing process enforced by CI. A typical pattern is a CI check that fails the build if catalog-info.yaml is missing, malformed, or references a non-existent owner team. Without this kind of enforcement, catalog entropy sets in within months as new services get created without registration and existing entries drift out of date as ownership changes.

3.4 Modeling Systems and Domains

Beyond individual components, Backstage's System and Domain entities let you model architecture at higher levels of abstraction — a System groups related components (e.g., "payments" containing payments-api, payments-worker, and payments-db), and a Domain groups related systems under a business capability (e.g., "commerce"). This hierarchical modeling becomes valuable once an org has more than a few dozen services, because it lets new engineers orient themselves at the right altitude — "what systems exist in commerce" — before drilling into individual component details.

3.5 Plugins and Extensibility

Backstage's plugin architecture is what lets the portal become a genuine single pane of glass rather than just a catalog browser. Common plugin categories:

  • CI/CD status (GitHub Actions, Jenkins, ArgoCD, CircleCI) surfaced directly on each component's page, so a developer can see build and deployment status without leaving the portal.
  • Cost visibility (cloud spend per service, often via integration with a FinOps tool or direct cloud billing API), turning cost from an abstract monthly report into a per-service, per-team signal.
  • Security posture (vulnerability scan results, SBOM data, dependency freshness) giving each team a live view of their own risk surface.
  • On-call/incident (PagerDuty, Opsgenie integration) showing who's currently on call for a service and recent incident history.
  • DORA and engineering metrics: deployment frequency, lead time, change failure rate, fed directly into the catalog so teams can see their own performance trends without separate tooling.

Building a custom plugin is a reasonable investment once you have two or three concrete, recurring developer pain points that existing community plugins don't solve. Resist the urge to build plugins speculatively — a common pattern in struggling platform teams is investing heavily in a plugin nobody asked for while the basics (a populated catalog, one working golden path) remain unfinished.

3.6 RBAC and Multi-Tenancy

As Backstage adoption grows, access control becomes a real concern: not every developer should be able to trigger every scaffolder template, and not every catalog entity should be editable by anyone who happens to find it. Backstage's permission framework allows fine-grained policies — for example, restricting "provision a production database" templates to senior engineers or requiring an approval step, while keeping "create a new service" open to everyone.

A reasonable default model:

  • Catalog read access: open to all employees — visibility is part of the point.
  • Catalog write access: scoped to the owning team, enforced by matching the authenticated user's group membership against the entity's owner field.
  • Scaffolder template execution: tiered by risk — low-risk templates (new service, new docs page) open to all; higher-risk templates (production infra changes, IAM grants) gated behind either a role check or a lightweight approval step embedded in the template itself.

Getting this right early avoids a common later-stage problem where the platform team becomes a de facto approval bottleneck for every template, recreating the exact ticket queue the IDP was meant to eliminate.

4. Golden Paths: Paving the Way

4.1 What a Golden Path Is

A golden path is the happy path for a common task — "create a new microservice," "add a Kafka topic," "spin up a preview environment" — implemented so that following it requires no infrastructure knowledge, and deviating from it is possible but explicitly unsupported (meaning: still allowed, but the platform team won't optimize for it, and the developer takes on more operational responsibility by going around the path).

The key word is opinionated. A golden path is not a menu of options; it's a small number of well-tested defaults. Offering twelve database choices in a template defeats the purpose — offer one or two, with a clearly signposted escape hatch for genuine exceptions. The temptation to make templates maximally flexible, to accommodate every possible team's preference, is one of the most reliable ways to turn a golden path into a configuration maze that takes longer to use correctly than the manual process it was meant to replace.

4.2 Designing Templates with the Scaffolder

Backstage's Scaffolder executes templates defined in YAML, composed of parameterized input forms and a sequence of actions: fetch a skeleton repo, template variables into it, create a source control repo, register it in the catalog, and trigger an initial CI run.

A minimal "new service" template:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
 name: new-go-service
 title: New Go Microservice
 description: Creates a production-ready Go service with CI, observability, and catalog registration pre-wired.
spec:
 parameters:
   - title: Service details
     required: [name, owner, system]
     properties:
       name:
         type: string
         description: Service name (lowercase, hyphenated)
       owner:
         type: string
         ui:field: OwnerPicker
       system:
         type: string
         ui:field: EntityPicker
         ui:options:
           catalogFilter:
             kind: System
 steps:
   - id: fetch
     name: Fetch skeleton
     action: fetch:template
     input:
       url: ./skeleton
       values:
         name: ${{ parameters.name }}
         owner: ${{ parameters.owner }}
   - id: publish
     name: Create repository
     action: publish:github
     input:
       repoUrl: github.com?repo=${{ parameters.name }}
       defaultBranch: main
   - id: register
     name: Register in catalog
     action: catalog:register
     input:
       repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
       catalogInfoPath: /catalog-info.yaml
 output:
   links:
     - title: Repository
       url: ${{ steps.publish.output.remoteUrl }}
     - title: Catalog entry
       icon: catalog
       entityRef: ${{ steps.register.output.entityRef }}

The skeleton repo this references is where the real value lives, and it should already include: a working Dockerfile, a complete CI pipeline configuration (lint, test, build, scan, deploy stages), baseline observability instrumentation (structured logging, a metrics endpoint, trace propagation already wired into the HTTP client), a catalog-info.yaml pre-filled with the parameterized values, and security defaults (non-root container user, dependency scanning enabled, secrets sourced from a vault rather than environment files). The golden path's value isn't the repository creation step itself — it's everything bundled inside the skeleton that the developer didn't have to research, decide on, or configure manually.

4.3 Golden Paths Beyond "New Service"

Once the new-service path exists and has real adoption, the same scaffolding pattern extends naturally to other recurring, high-friction tasks:

  • Provisioning a database, delegating to Terraform or Crossplane underneath (see Section 5), with the template collecting only the handful of parameters that actually vary (size tier, environment) while everything else — encryption, backup schedule, monitoring — is fixed by the underlying module.
  • Spinning up ephemeral preview environments per pull request, useful for review and QA without consuming persistent infrastructure.
  • Onboarding a service to the service mesh, handling sidecar injection configuration and mTLS setup that's easy to get subtly wrong by hand.
  • Requesting a new Kafka topic or message queue, with naming conventions and retention policy defaults baked in.
  • Rotating credentials or requesting new IAM roles within pre-approved boundaries, replacing what's often a manual, ticket-driven security process with a bounded self-service flow.
  • Decommissioning a service, which is frequently neglected but equally valuable — a golden path for safely tearing down infrastructure, removing DNS entries, and archiving the catalog record prevents the slow accumulation of orphaned resources that nobody remembers creating.

4.4 Governance Without Friction

Golden paths are also a governance mechanism, and this is worth stating explicitly because it reframes how security and compliance teams should think about engaging with the platform. By making the paved path the easiest path — easier than going around the platform — you achieve compliance through convenience rather than mandate. Encoding requirements like mandatory resource tagging, encryption-at-rest, or approved base images directly into templates means policy is enforced structurally, at the moment of creation, rather than discovered later through an audit and remediated after the fact at much higher cost.

This doesn't eliminate the need for guardrail enforcement elsewhere (see Section 6 on policy-as-code), since developers can and sometimes will go around the golden path for legitimate reasons. But it shifts the large majority of compliance work left, into the default path, where it costs the developer nothing extra and costs the security team far less ongoing audit effort.

4.5 Template Lifecycle and Versioning

Golden paths are not "build once, done" artifacts. As the org's standards evolve — a new base image, an updated CI tool, a new required security scan — templates need to be versioned and existing services need a path to adopt the update. A practical approach is treating the skeleton repo itself as a dependency: services generated from a template retain a reference to the template version they were scaffolded from, and a periodic (often quarterly) "golden path upgrade" pass — sometimes automated as a bot-generated pull request — proposes the delta to each consuming repo. Without this mechanism, golden paths calcify: the template stays frozen at whatever standards existed when it was first written, while best practices for everything it encodes continue to move forward elsewhere.

5. Self-Service Infrastructure

5.1 The Provisioning Problem

Golden paths for code are necessary but insufficient — developers also need infrastructure: databases, queues, storage buckets, caches, secrets. The traditional model routes these requests through tickets to an infrastructure team, with turnaround measured in days. Self-service infrastructure replaces the ticket with an API or form, backed by automation that still respects organizational policy, with turnaround measured in minutes.

5.2 Infrastructure-as-Code as the Foundation

Whatever the front-end mechanism, the underlying provisioning logic should be expressed declaratively, typically via one of two complementary approaches:

  • Terraform modules, version-pinned and published internally, each encoding a "blessed" configuration of a resource — for example, an RDS instance module that always applies the org's standard backup window, encryption settings, parameter group, and CloudWatch alarms, exposing only a handful of variables (size, environment, name) to the caller.
  • Crossplane compositions, which let you define custom Kubernetes-native APIs (e.g., a Database custom resource) that compile down to the underlying cloud resources via a controller. This fits especially naturally into a Kubernetes-centric platform, since the same kubectl apply / GitOps mental model used for application deployment extends directly to infrastructure provisioning.

The principle in both cases is identical: developers shouldn't write raw Terraform or click around a cloud console. They request a higher-level abstraction — "give me a Postgres database, small tier, with backups" — and the platform translates that request into compliant, lower-level resources on their behalf, with the complexity of the underlying cloud APIs entirely hidden.

5.3 Connecting Backstage to the Provisioning Layer

Backstage Scaffolder templates become the front door to this layer. A template for "new database" collects a small number of parameters (engine, size tier, environment) and then takes one of two architectural paths:

  1. Opens a pull request against a GitOps repository containing the Terraform or Crossplane manifests, which is then applied by a CD pipeline (ArgoCD, Flux, or Atlantis) after automated policy checks pass — the GitOps pattern, generally preferred for its auditability and reversibility.
  2. Calls an internal provisioning API directly, which is faster (no pull-request review latency) but requires the API itself to own validation, policy enforcement, and audit logging that would otherwise come for free from version control.

The GitOps pattern is generally preferable for production infrastructure: every change is a reviewable, revertible artifact in version control, and the "who changed what, when, and why" question is answered for free by git history and commit messages, which matters enormously during incident postmortems. The direct-API pattern earns its keep for lower-risk, high-frequency operations — e.g., short-lived preview environments — where the overhead of a pull-request cycle outweighs the benefit.

5.4 Policy-as-Code Guardrails

Self-service only works if "no human bottleneck" doesn't mean "no checks." Policy engines — Open Policy Agent (OPA, often via its Conftest tooling), Kyverno for Kubernetes-native resources, or HashiCorp Sentinel for Terraform — validate generated manifests automatically before they're applied: enforcing tagging standards, blocking public S3 buckets, capping instance sizes by environment, requiring encryption at rest, restricting which regions resources can be created in.

These checks run automatically in the CI/CD pipeline as a required step before apply, so the human reviewer (if a review step exists at all) is reviewing business intent — "does this team genuinely need a large instance" — rather than manually checking for compliance details a machine can verify far more reliably. A representative OPA policy might look like:

package terraform.policies

deny[msg] {
 resource := input.resource_changes[_]
 resource.type == "aws_s3_bucket"
 not resource.change.after.tags["owner"]
 msg := sprintf("S3 bucket %v is missing required 'owner' tag", [resource.address])
}

5.5 Cost and Capacity Awareness

Self-service infrastructure without cost visibility tends to produce sprawl, since the natural friction that previously made someone think twice before requesting a large resource (waiting for a ticket, justifying the request to a human) disappears. Two practices mitigate this:

  • Surface estimated cost before provisioning, inside the Backstage template itself, via a cost-estimation step (tools like Infracost integrate well here) so the requester sees the monthly cost implication of their choice before submitting.
  • Tag every provisioned resource with owning team and service automatically as part of the template logic — never as a manual field developers might skip — enabling accurate showback or chargeback reporting later without relying on tagging discipline that inevitably erodes over time.

5.6 Handling the Long Tail

No template library will cover every infrastructure need, and a mature self-service platform needs an explicit process for the long tail of requests that don't fit existing golden paths — a "request a new resource type" path that routes to the platform team, distinct from "I have a question," with a service-level expectation attached. Treating these as product feedback (candidates for the next golden path to build) rather than one-off tickets to dispatch and forget keeps the template library growing in the direction of actual demand.

6. Platform Engineering as a Product Discipline

6.1 Treat the Platform as a Product, Developers as Customers

The most common reason IDP initiatives stall isn't technical — it's that the platform team builds what they assume developers need rather than what developers actually struggle with. Apply standard product practices:

  • Interview a representative sample of engineering teams before building anything, and keep interviewing periodically afterward — pain points shift as the org and its tech stack evolve.
  • Define an internal "north star" metric — commonly reducing time-to-first-deploy for a new service, or reducing toil hours per sprint as self-reported by teams.
  • Ship incrementally: one golden path, measure and grow adoption, iterate based on feedback, then add the next. Resist the temptation to build five golden paths simultaneously before any of them is genuinely excellent.
  • Track adoption like a product metric — if a golden path has near-zero usage six weeks after launch, that's a signal to investigate (was it announced properly? does it actually solve the problem? is the old manual path still easier?), not a footnote to ignore.

6.2 Measuring Platform Success

Useful metrics fall into two categories.

Outcome metrics (the org-level effects you ultimately care about):

  • DORA metrics — deployment frequency, lead time for changes, change failure rate, time to restore service — tracked per team and in aggregate, ideally surfaced directly in Backstage so teams see their own trend lines.
  • Time from "developer starts a new service" to "first production deploy," a concrete, easily understood proxy for how much friction the platform has removed.

Adoption and health metrics (leading indicators that predict the outcome metrics above):

  • Percentage of services onboarded to the catalog with complete, valid metadata.
  • Golden path usage rate relative to manual or bespoke provisioning for the same task.
  • Developer satisfaction, gathered via periodic lightweight surveys — a simple "platform NPS" question asked quarterly works well and is cheap to maintain.
  • Mean time to resolve platform-reported issues, treating the platform team's own incident response quality as a first-class metric rather than an afterthought.

Avoid vanity metrics like raw Backstage login counts — they correlate poorly with actual value delivered and can be inflated trivially (e.g., by making the portal the company homepage) without reflecting genuine usefulness.

6.3 Organizational Rollout and Change Management

Technical excellence alone doesn't guarantee adoption; how a platform is introduced matters nearly as much as what it contains.

  • Start with a willing pilot team, ideally one with a real, acute pain point the platform addresses, rather than mandating adoption org-wide on day one. A successful pilot generates internal advocates whose testimonials carry far more weight than the platform team's own marketing.
  • Avoid mandates before the product earns them. Forcing adoption of an unproven golden path breeds resentment and encourages workarounds; let early wins create organic pull instead.
  • Invest in onboarding materials and office hours, especially in the first few months — a golden path that's technically excellent but undiscoverable or confusingly documented will sit unused.
  • Create a visible feedback channel (a dedicated Slack channel, a lightweight RFC process for proposed new templates) so developers feel ownership over the platform's direction rather than feeling it was imposed on them.
  • Sunset old paths deliberately. Once a golden path has clear adoption and is meaningfully better than the manual alternative, set a timeline for deprecating the old way — including removing old documentation and disabling unmaintained tickets queues — so the org doesn't end up maintaining two parallel systems indefinitely.

6.4 Avoiding the Common Failure Modes

  • Building infrastructure instead of glue. If the platform team finds itself provisioning Kubernetes clusters by hand instead of building self-service templates over existing infra, the platform initiative has scope-crept into being just another infra team, and the original goal of developer self-service has quietly been abandoned.
  • Mandating instead of paving. Golden paths that are mandatory but ergonomically worse than the old way will be circumvented or resented, and the resentment tends to spill over into skepticism about the platform team's future initiatives too.
  • Catalog rot. Stale or incomplete catalog data erodes trust quickly; once developers stop trusting the catalog as accurate, they stop using it for decisions, and the central premise of the IDP — a single reliable source of truth — collapses even if the underlying technology still works fine.
  • Underinvesting in templates' "blessed" content. A scaffolder template that produces a bare-bones repo without working CI, observability, and security defaults baked in delivers only a fraction of the possible value — the unglamorous work of building genuinely excellent skeleton repos is where most of the platform's ROI actually lives, even though it's less visible than the portal UI itself.
  • No clear platform team ownership model. Platform teams need to operate with the rigor of a product team: a roadmap communicated to stakeholders, an on-call rotation for the portal and its critical templates, and a structured feedback loop — not as an under-resourced side project bolted onto an already-stretched infra team.
  • Ignoring the platform team's own developer experience. The team building the IDP is itself a team of developers, and applying the same product discipline to their internal tooling (CI for the templates themselves, testing for Terraform modules, staging environments for the portal) prevents the platform from becoming the least reliable, least tested piece of software in the company — an ironic but common outcome.

7. A Practical Rollout Sequence

For organizations starting from scratch, a reasonable sequencing is below, with rough timeline and staffing figures attached. These numbers assume a mid-sized organization (roughly 150-400 engineers, a few dozen services) and a dedicated platform team of 2-4 engineers; adjust up or down for scale, but the relative proportions — most of the early budget going to one excellent golden path rather than breadth — tend to hold regardless of company size.

  1. Stand up Backstage with SSO, PostgreSQL, and basic hosting. No plugins beyond the defaults yet — resist scope creep at this stage. Roughly 2-4 weeks for a team that's already comfortable operating Kubernetes services; add a few weeks if SSO integration involves an unfamiliar identity provider. Infrastructure cost here is minor — typically a few hundred dollars a month in compute and a small managed Postgres instance.
  2. Populate the catalog for existing services — ideally enforced going forward via a CI check requiring a valid catalog-info.yaml in every repo, paired with a focused, time-boxed effort to backfill existing services rather than letting backfill drag on indefinitely. Roughly 3-6 weeks, scaling with service count; for 30-50 services this is usually a few days of scripting to auto-generate a baseline catalog-info.yaml from existing repo metadata, plus a couple of weeks of chasing down ownership for the stragglers.
  3. Ship one golden path end-to-end: typically "new service," because it has the highest visibility, the clearest before/after contrast, and naturally seeds catalog growth as a side effect of every use. This is the single largest early investment — budget 4-8 weeks, most of it spent on the skeleton repo (CI pipeline, observability instrumentation, security defaults) rather than the Scaffolder YAML itself, which is comparatively quick to write.
  4. Add TechDocs so documentation has a single discoverable home, increasing the portal's everyday utility beyond just the catalog and giving developers a reason to visit it even when they're not creating something new. 1-2 weeks to wire up the plugin and establish a documentation template; ongoing cost is mostly cultural (getting teams to actually write docs) rather than technical.
  5. Introduce self-service infrastructure for the single highest-friction resource type (commonly databases), wired through GitOps and policy-as-code from the very start rather than retrofitted later. 6-10 weeks, typically the second-largest investment after the first golden path — Terraform/Crossplane module authoring, GitOps repo and CD pipeline setup, and initial OPA policy writing each take real time, and this is also where teams most often underestimate the policy-as-code piece specifically.
  6. Layer in plugins addressing the next-highest-friction pain points identified through ongoing developer interviews — CI visibility, cost data, on-call status are common early choices. 1-3 weeks per plugin for well-supported community plugins; budget more (4-6 weeks) for anything custom-built.
  7. Instrument and iterate, using DORA and adoption metrics to prioritize the next golden path or infrastructure type to onboard, treating the roadmap as continuously informed by data rather than fixed at launch. This is ongoing rather than a discrete phase, but a reasonable checkpoint is a dashboard review at the 3-month and 6-month marks to decide what's next.

Put together, a realistic time-to-first-value — Backstage live, catalog populated, one golden path in daily use — is roughly 3-4 months for a team of this size, with self-service infrastructure following another 6-10 weeks behind that. Organizations frequently underestimate step 3 specifically, treating the Scaffolder template as the hard part when it's usually the skeleton repo's production-readiness that consumes the bulk of the time. This sequencing front-loads trust-building — a working catalog, one genuinely excellent golden path — before expanding scope, which tends to produce durable, organic adoption rather than a portal that launches with fanfare and is quietly abandoned within a quarter once the novelty wears off.

8. Worked Example: The notifications-api Service, Start to Finish

The sections above describe the platform's layers individually; this section follows one realistic service through all of them as a single connected story, with the actual artifacts produced at each step, so the pieces can be seen fitting together rather than just described in the abstract.

8.1 Week 1 — Scaffolding

Priya, an engineer on the Growth team, needs a new service to handle push and email notifications. She opens Backstage and runs the new-go-service template from Section 4.2, submitting these values through the form:

# Values submitted to the Scaffolder via the template's input form
name: notifications-api
owner: team-growth
system: growth

The Scaffolder executes the template's steps against these values, and within about ninety seconds she has a new repository, org/notifications-api, generated from the skeleton. The generated catalog-info.yaml at the repo root looks like this:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
 name: notifications-api
 description: Handles push and email notification delivery
 annotations:
   github.com/project-slug: org/notifications-api
   backstage.io/techdocs-ref: dir:.
 tags:
   - growth
spec:
 type: service
 lifecycle: production
 owner: team-growth
 system: growth
 dependsOn:
   - resource:notifications-db

The skeleton also generates a working CI pipeline. The .github/workflows/ci.yaml produced looks like this:

name: CI
on:
 pull_request:
 push:
   branches: [main]

jobs:
 build:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v4

     - uses: actions/setup-go@v5
       with:
         go-version: "1.22"

     - name: Lint
       run: golangci-lint run ./...

     - name: Test
       run: go test ./... -coverprofile=coverage.out

     - name: Build
       run: go build -o bin/notifications-api ./cmd/server

     - name: Scan image
       run: trivy image --exit-code 1 --severity CRITICAL,HIGH notifications-api:${{ github.sha }}

     - name: Push image
       if: github.ref == 'refs/heads/main'
       run: |
         docker build -t registry.internal/notifications-api:${{ github.sha }} .
         docker push registry.internal/notifications-api:${{ github.sha }}

And a minimal, already-instrumented HTTP entrypoint, cmd/server/main.go:

package main

import (
"log/slog"
"net/http"
"os"

"github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

logger.Info("starting notifications-api", "port", 8080)
if err := http.ListenAndServe(":8080", mux); err != nil {
logger.Error("server exited", "error", err)
os.Exit(1)
}
}

Priya adds her actual notification-sending handlers to this scaffold that afternoon, rather than spending the day assembling logging, metrics, Dockerfiles, and CI from scratch.

8.2 Week 1, later that day — Provisioning

The service needs a Postgres database for notification preferences and delivery history. Priya runs the "new database" golden path from Section 5.3, submitting:

# Values submitted to the "new database" Scaffolder template
service: notifications-api
engine: postgres
size: small
environment: staging

This template step opens a pull request against the platform team's GitOps repo, adding the following Crossplane claim:

# gitops-repo/claims/notifications-db.yaml
apiVersion: platform.org/v1alpha1
kind: Database
metadata:
 name: notifications-db
 labels:
   owner: team-growth
   service: notifications-api
spec:
 engine: postgres
 size: small
 environment: staging

This claim is validated against the platform team's existing Database composition, which fixes the settings the org always requires regardless of what the requester specified:

# gitops-repo/compositions/database-composition.yaml (excerpt, pre-existing)
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
 name: postgres-standard
spec:
 compositeTypeRef:
   apiVersion: platform.org/v1alpha1
   kind: Database
 resources:
   - name: rds-instance
     base:
       apiVersion: rds.aws.upbound.io/v1beta1
       kind: Instance
       spec:
         forProvider:
           engine: postgres
           storageEncrypted: true
           backupRetentionPeriod: 7
           deletionProtection: true
         tags:
           managed-by: crossplane

The pull request triggers the OPA policy check from Section 5.4 automatically as a CI job:

$ conftest test claims/notifications-db.yaml --policy policies/
PASS - claims/notifications-db.yaml - terraform.policies - owner tag present
PASS - claims/notifications-db.yaml - terraform.policies - encryption enabled
PASS - claims/notifications-db.yaml - terraform.policies - environment is valid

3 tests, 3 passed, 0 warnings, 0 failures

Because the check passes, a platform engineer merges the PR after a thirty-second glance — the real verification already happened in CI, not in their head. ArgoCD detects the merge and reconciles the actual RDS instance within a few minutes; Priya receives a Slack notification from the platform's bot once the claim reaches Ready: True.

8.3 Week 2 — Deployment

Database credentials are delivered to the running service via the platform's standard secrets-injection convention, so no manual credential handling is required. Priya's Kubernetes deployment manifest, generated as part of the skeleton, already references it correctly:

# k8s/deployment.yaml (excerpt, from the skeleton)
apiVersion: apps/v1
kind: Deployment
metadata:
 name: notifications-api
spec:
 replicas: 2
 template:
   spec:
     containers:
       - name: notifications-api
         image: registry.internal/notifications-api:latest
         envFrom:
           - secretRef:
               name: notifications-db-credentials  # auto-populated by the platform's secrets operator
         ports:
           - containerPort: 8080

Her first merged pull request runs the CI pipeline shown in 8.1: tests pass, Trivy reports no critical or high vulnerabilities, and the pipeline promotes the build to staging automatically. The notifications-api catalog page now shows a green CI badge, a link to the running staging environment, and — via the cost-visibility plugin — an estimated $340/month in infrastructure spend, computed from the size: small RDS instance and the two-replica deployment, tagged automatically with owner: team-growth for the Growth team's cost dashboard.

8.4 Month 3 — Steady state

In production, notifications-api's catalog page surfaces its DORA metrics, pulled from the CI/CD and incident systems via the DORA plugin:

{
 "component": "notifications-api",
 "deployment_frequency": "1.1 / day",
 "lead_time_for_changes": "1h 42m",
 "change_failure_rate": "3.2%",
 "time_to_restore_service": "24m"
}

These numbers are consistent with the org-wide averages for services built on the same golden path — a useful signal to the platform team that the path is performing as intended rather than producing a one-off success.

8.5 Month 8 — Template upgrade

The platform team updates the new-go-service skeleton's base image, per the template versioning practice from Section 4.5. An automated bot, tracking which repos were scaffolded from which template version, opens a pull request against every consuming repo, including notifications-api. The generated diff is small and mechanical:

--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.22-bookworm AS build
+FROM golang:1.22-bookworm@sha256:9f8e1a2b... AS build
WORKDIR /src
COPY . .
RUN go build -o /out/notifications-api ./cmd/server

-FROM gcr.io/distroless/base-debian11:latest
+FROM gcr.io/distroless/base-debian12:nonroot
COPY --from=build /out/notifications-api /notifications-api
+USER nonroot:nonroot
ENTRYPOINT ["/notifications-api"]

Priya's team reviews the five-line diff and merges it the same day. No ticket was filed, no manual audit was needed to discover that notifications-api was running an outdated base image, and no cross-team meeting had to happen to coordinate the same rollout across the dozens of other services generated from this template — the bot opened the equivalent PR against each of them independently.

What's notable across this story is not any single artifact in isolation, but the absence of friction connecting them: Priya never filed a ticket, never waited on another team, and never had to hand-write a Dockerfile, CI pipeline, Crossplane claim, or Kubernetes manifest from scratch — while the platform team retained full visibility into cost, security posture, and compliance the entire time, verified by machine-checked policy rather than manual review.

9. Troubleshooting Common Adoption Problems

Even well-built platforms run into predictable friction points. A few patterns and responses worth anticipating:

  • "Nobody's using the golden path." Check three things in order: discoverability (do developers know it exists?), correctness (does it actually solve the problem better than the status quo?), and trust (has a previous bad experience with the platform made people wary?). The fix is usually communication and iteration, not a technical rewrite.
  • "The catalog is full of stale entries." This almost always traces back to missing CI enforcement. Retrofit a validation check rather than attempting a one-time manual cleanup, which will simply decay again.
  • "Self-service infra requests are still slow." Often the bottleneck has moved from the original ticket queue to a policy-check or approval step that's now the new chokepoint. Treat this the same way you'd treat any other slow step in a pipeline — measure it, and optimize or remove it.
  • "Teams are building their own bespoke tooling instead of using the platform." This is a strong signal the platform doesn't yet serve their specific use case well. Investigate before discouraging the behavior — sometimes it reveals a genuine gap worth closing with a new golden path.

10. Conclusion

An Internal Developer Platform succeeds or fails less on tooling choice and more on discipline: treating the platform as a product, keeping golden paths genuinely opinionated rather than sprawling into configuration menus, and ensuring self-service infrastructure remains both fast and policy-compliant by construction rather than by review. Backstage provides a strong, extensible foundation for the experience layer, but the platform's real value is created in the layers underneath and around it — the quality of the skeleton repos, the soundness of the Terraform and Crossplane modules, the policy-as-code guardrails that make speed and safety compatible, and the organization's willingness to keep iterating based on what developers actually need rather than what seemed elegant at design time.

Done well, an IDP becomes close to invisible: developers stop thinking about infrastructure because the paved path handles it, and the platform team's success is measured by how little anyone has to think about them at all.

Appendix A: Glossary

  • Golden path — An opinionated, well-supported default workflow for a common engineering task.
  • Scaffolder — Backstage's templating engine for executing multi-step, parameterized workflows.
  • GitOps — A pattern where infrastructure and application state are declared in version control and reconciled automatically by an operator (e.g., ArgoCD, Flux).
  • Policy-as-code — Expressing compliance and security rules as machine-evaluated code (e.g., OPA/Rego policies) rather than manual review checklists.
  • DORA metrics — Deployment frequency, lead time for changes, change failure rate, and time to restore service; the widely used industry benchmark for software delivery performance.
  • Crossplane composition — A Kubernetes-native abstraction that lets platform teams define custom infrastructure APIs backed by cloud resources.

Appendix B: Pre-Launch Checklist

  • [ ]  SSO integrated and tested with the production identity provider
  • [ ]  PostgreSQL backing store provisioned with backups enabled
  • [ ]  At least one golden path fully built, including a production-quality skeleton repo
  • [ ]  CI enforcement in place for catalog-info.yaml validity
  • [ ]  Policy-as-code checks wired into the self-service infra pipeline
  • [ ]  A feedback channel and lightweight RFC process established
  • [ ]  Adoption and DORA metrics dashboards defined before launch, not after
  • [ ]  A pilot team identified and onboarded ahead of org-wide rollout