Internal Developer Platforms: Stop Reinventing Wheels in Every Squad

A few years ago I sat down with my teams to work out why we were constantly behind on product delivery. We had good engineers, a clear roadmap, and genuine motivation to ship. But every quarter looked the same: ambitious goals, modest output, and a growing sense of frustration on both sides.
When I dug into where the time actually went, the answer was uncomfortable. Team after team was spending the majority of their sprint capacity not on the features our customers were waiting for, but on building and maintaining infrastructure. Deployment pipelines. Logging setups. Secret management. Environment configuration. Each squad had quietly become a miniature platform team, solving problems that had nothing to do with our actual business value.
What made it worse was that they were all solving the same problems, independently, at the same time. This is the modern software development paradox: we preach "don't reinvent the wheel," yet our organizations are running wheel factories on every floor.
The Expensive Hobby of Reinvention
Most development teams spend 30-40% of their time building and maintaining infrastructure that has nothing to do with their actual business value. They are not building features that delight customers or solve unique problems. They are wrestling with Kubernetes configurations, debugging CI/CD pipelines, and creating the 47th iteration of a secrets management solution.
It's expensive, inefficient, demoralizing, and ultimately unsustainable. Martin Fowler once noted that "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." But what about writing no code at all for problems already solved? That's where Internal Developer Platforms (IDPs) enter the conversation.
An IDP is the golden path your organization provides to get from idea to production. It's the curated set of tools, workflows, and self-service capabilities that let developers focus on what makes your business unique, rather than becoming amateur DevOps engineers.
Think of It as a Buffet, Not a Kitchen
Imagine walking into a restaurant where every diner must first build their own kitchen before they can eat. Absurd, right? Yet that's exactly what happens in organizations without an IDP.
An Internal Developer Platform is like a well-stocked buffet. The platform team (the chefs) has already prepared the foundational dishes: deployment pipelines, observability tools, database provisioning, secrets management, and networking configurations. Developers (the diners) can self-service from this buffet, combining elements to create their applications without needing to understand the intricate recipes behind each dish.
The key difference between an IDP and just "some DevOps tooling" is the self-service aspect combined with organizational standards. An IDP provides:
- Golden paths: Opinionated, pre-approved ways to accomplish common tasks
- Self-service capabilities: Developers can provision resources without tickets or waiting
- Guardrails, not gates: Sensible defaults and constraints that maintain compliance while enabling speed
- Abstraction layers: Hide complexity while allowing escape hatches for advanced use cases
Think of it as providing a well-maintained highway system rather than forcing every team to build their own roads from scratch.
The Anatomy of an Effective IDP
An IDP is an ecosystem of integrated capabilities that work together. Let's break down the essential components:
The Core Pillars
| Component | Purpose | Examples |
|---|---|---|
| Application Configuration | Define and manage application metadata, dependencies, and runtime requirements | Backstage, Port, internal YAML schemas |
| Infrastructure Orchestration | Provision and manage infrastructure resources | Terraform, Crossplane, Pulumi |
| CI/CD Pipelines | Automate build, test, and deployment workflows | GitHub Actions, GitLab CI, Jenkins, Argo CD |
| Observability Stack | Monitor, log, and trace applications | Prometheus, Grafana, ELK Stack, Datadog |
| Developer Portal | Central interface for discovery and self-service | Backstage, custom portals |
| Security & Compliance | Secrets management, policy enforcement, scanning | HashiCorp Vault, OPA, Snyk |
The Three Layers of Abstraction
A well-designed IDP separates concerns into three distinct layers, each with a clear owner and a clear audience.
The foundation at the bottom is the platform team's domain. Cloud resources, Kubernetes clusters, networking, DNS, IAM, and container registries all live here. Application developers have no reason to touch this layer directly, and a good IDP ensures they never have to.
The service layer in the middle is where the platform's value becomes concrete. CI/CD golden paths, infrastructure orchestration via Terraform or Crossplane, observability through Prometheus and Grafana, secrets management with Vault, database provisioning on demand. Developers reach this layer through API calls or CLI commands, not through tickets or Slack messages to an ops team.
The developer experience layer at the top is what engineers actually see. The Backstage portal, the service catalogue, the self-service scaffolder, deployment status dashboards, CLI and IDE integrations. This is the face of the platform. If it is slow, confusing, or incomplete, teams will route around the whole thing regardless of how solid the layers beneath it are.
The arrow between Layer 3 and Layer 2 in the diagram says "API Calls / CLI Commands." That label matters. The moment a developer has to raise a ticket or wait for a human to provision something, you have broken the contract. Everything in the service layer should be reachable programmatically, on demand, without asking permission.
The Goldilocks Problem: Not Too Rigid, Not Too Chaotic
The biggest challenge in IDP design is finding the right balance:
Too prescriptive: "You must use these exact tools in this exact way" leads to rebellion. Teams with legitimate edge cases will route around your platform, creating shadow IT.
Too permissive: "Here are 50 tools, good luck" creates the chaos you were trying to avoid. Decision paralysis and inconsistency reign.
Just right: "Here's the golden path for 80% of use cases, with clear escape hatches for the other 20%" enables both speed and flexibility.
The secret is making the golden path so smooth and friction-free that choosing the alternative feels like extra work, not like fighting the system.
Rolling Out Your Platform
An IDP does not have to start big. The goal is to solve one pain point so well that teams want more. Here is a path you can follow entirely from your local workstation, using Backstage, the open-source developer portal originally built at Spotify and now a CNCF project.
Prerequisites: Docker Desktop, Node.js 22 or 24, and a GitHub account. On Windows, run corepack enable once in an Administrator PowerShell before anything else.
Before starting the server, you need three environment variables set in the same terminal you will use for yarn start. Two come from a GitHub OAuth App (for browser sign-in), and one is a Personal Access Token (for the scaffolder to create repositories via the API). These are separate credentials that serve different purposes. More on that below.
Create the OAuth App: Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App and set the Homepage URL to http://localhost:3000 and the Authorization callback URL to http://localhost:7007/api/auth/github/handler/frame. Copy the Client ID and generate a Client Secret.
Create the PAT: Go to GitHub Settings > Developer settings > Personal access tokens > Tokens (classic) and generate a token with the repo and workflow scopes.
Now set all three:
# macOS / Linux
export AUTH_GITHUB_CLIENT_ID=your_oauth_client_id
export AUTH_GITHUB_CLIENT_SECRET=your_oauth_client_secret
export GITHUB_TOKEN=ghp_your_pat
# Windows PowerShell
$env:AUTH_GITHUB_CLIENT_ID="your_oauth_client_id"
$env:AUTH_GITHUB_CLIENT_SECRET="your_oauth_client_secret"
$env:GITHUB_TOKEN="ghp_your_pat"
Step 1: Audit the Duplication
Before writing a single line of platform code, spend an hour mapping what your teams already have. List every squad and note how each handles deployments, logging, secrets, and environment setup. You will almost certainly find three or four teams solving the same problems independently with slightly different tools.
That list is your backlog. Sort it by frequency of pain and pick the top item. A common first finding: every squad has its own Dockerfile that is 90% identical. That is the wheel to consolidate first.
Step 2: Scaffold and Start Backstage
npx @backstage/create-app@latest
cd my-backstage-app
yarn start
The first run takes a few minutes. Once you see "Rspack compiled successfully", open http://localhost:3000. You will be prompted to sign in with GitHub. That is the OAuth App at work. After signing in you have a running developer portal. It is empty, which is fine.
Step 3: Configure GitHub Authentication in Backstage
The fresh Backstage install uses a guest provider. Replace it with GitHub OAuth so the portal knows who is using it.
Install the backend module:
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-github-provider
Register it in packages/backend/src/index.ts:
backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
Replace the auth section in app-config.yaml:
auth:
environment: development
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
signIn:
resolvers:
- resolver: usernameMatchingUserEntityName
dangerouslyAllowSignInWithoutUserInCatalog: true
The dangerouslyAllowSignInWithoutUserInCatalog flag is required for local development. Without it, the resolver looks for a User entity in the catalog whose name matches your GitHub username, finds nothing in the empty catalog, and fails with "unable to resolve user identity". Remove this flag in production and populate the catalog with real User entities instead.
Wire up the sign-in page in packages/app/src/App.tsx:
The sign-in page is an extension, not a feature. It must be wrapped in createFrontendModule. Passing it directly to the features array will not show the button.
import { createApp } from '@backstage/frontend-defaults';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import catalogPlugin from '@backstage/plugin-catalog/alpha';
import { navModule } from './modules/nav';
import { githubAuthApiRef } from '@backstage/core-plugin-api';
import { SignInPage } from '@backstage/core-components';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
const signInPage = SignInPageBlueprint.make({
params: {
loader: async () => props => (
<SignInPage
{...props}
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
}}
/>
),
},
});
export default createApp({
features: [
catalogPlugin,
navModule,
createFrontendModule({
pluginId: 'app',
extensions: [signInPage],
}),
],
});
Restart yarn start and the sign-in button will appear.
Step 4: Add Your First Software Template
Templates are the core of the self-service experience. A developer fills in a form, and Backstage scaffolds a complete repository with a CI/CD pipeline and health checks already wired up. No yak-shaving required.
Create templates/microservice/template.yaml in your Backstage app. Backstage does not auto-discover templates, so also register it explicitly in app-config.yaml:
# Inside catalog.locations in app-config.yaml
- type: file
target: ../../templates/microservice/template.yaml
rules:
- allow: [Template]
The template itself:
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: microservice
title: New Microservice
description: Scaffolds a service with CI/CD and health checks wired up
spec:
owner: platform-team
type: service
parameters:
- title: Service details
required: [name, owner, githubOrg]
properties:
name:
title: Service name
type: string
description: Lowercase, hyphens only (e.g. payment-processor)
owner:
title: Owning squad
type: string
githubOrg:
title: GitHub owner
type: string
description: Your GitHub username or organisation (e.g. my-org)
steps:
- id: fetch
name: Fetch base template
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
- id: publish
name: Create GitHub repository
action: publish:github
input:
repoUrl: github.com?owner=${{ parameters.githubOrg }}&repo=${{ parameters.name }}
defaultBranch: main
output:
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
A note on credentials: the OAuth App handles browser authentication, but publish:github is a backend action that calls the GitHub API directly. It reads its token from the integrations.github section in app-config.yaml, which resolves to the GITHUB_TOKEN environment variable. This is why both credentials are needed: they do different jobs.
The skeleton/ folder next to template.yaml holds your standard project structure: a Dockerfile, a GitHub Actions workflow, a health check endpoint, and a README. Every new service scaffolded from this template gets all of that for free, consistently, from day one.
Step 5: Wire Up a Golden-Path CI/CD Pipeline
Put a GitHub Actions workflow in skeleton/.github/workflows/deploy.yml. Every repository created from the template will include it automatically:
name: Build and deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t ${{ github.repository }}:${{ github.sha }} .
- name: Run tests
run: docker run --rm ${{ github.repository }}:${{ github.sha }} npm test
- name: Push to registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Deploy to staging
run: |
kubectl set image deployment/${{ github.event.repository.name }} \
app=ghcr.io/${{ github.repository }}:${{ github.sha }} \
--namespace staging
Squads write business logic. The platform handles the rest.
Step 6: Validate the Golden Path End to End
Before showing this to any team, walk through it yourself. Open http://localhost:3000/create, fill in the form, and let the scaffolder run. Then:
git clone https://github.com/your-org/test-service
cd test-service
ls -la
# Confirm: Dockerfile, .github/workflows/, src/, README.md are all present
echo "# test" >> README.md
git add . && git commit -m "test pipeline" && git push
Watch the Actions tab on GitHub. If the build, test, and push steps all go green, your golden path works. That is the demo you bring to your first early-adopter squad.
Step 7: Measure from Day One
Before rolling out to anyone, agree on three numbers you will track every week:
- Time to first deployment: from "new repo created" to "service running in staging." Aim for under 10 minutes.
- Deployment frequency: how many times per week does a squad push to production? Set a baseline before the IDP; anything higher is a win.
- Platform support load: how many Slack messages or tickets does the platform team receive per squad per week? A rising number means the golden path has gaps that need fixing.
Put these on a shared dashboard and show them publicly. Teams respond to visible evidence that the platform is saving them real time.
Step 8: Find Champions, Not Mandates
Take your working golden path to the one squad that is loudest about infrastructure pain. Do not announce it company-wide. Sit with them, walk through the scaffolding, let them break things, and fix the rough edges together.
When that squad ships their next feature in half the time, the other squads will ask how. That word-of-mouth is worth more than any top-down directive. Add each new adopter to a #platform-users Slack channel where they can share tips and report issues directly. The platform evolves through that conversation, not through a quarterly roadmap review.
The Payoff
When done right, an Internal Developer Platform delivers measurable business impact:
- Faster time to market: Reduce deployment times from days to minutes
- Improved developer experience: Engineers spend time solving business problems, not fighting infrastructure
- Consistency and compliance: Security and governance baked in, not bolted on
- Cost optimization: Eliminate duplicate tooling and wasted infrastructure
- Knowledge retention: When a DevOps expert leaves, their expertise lives on in the platform
One mid-sized e-commerce company implemented an IDP and saw deployment frequency increase from weekly to multiple times per day. Their lead time for changes dropped from two weeks to four hours. Developer satisfaction scores jumped 40%. The platform team of five enabled 200 developers to move faster than they ever had.
Your Next Move
Stop accepting the status quo of reinvention. Here's your action plan:
- Audit your current state: How many teams are solving the same infrastructure problems independently? What's the actual cost in time and money?
- Start small and focused: Pick one pain point and solve it exceptionally well. Build momentum through wins, not comprehensive plans.
- Think product, not project: Your IDP is never "done." It evolves with your organization's needs.
- Measure what matters: Track time saved, deployment frequency, and developer happiness. Use data to justify continued investment.
The question is not whether you need an Internal Developer Platform. If you have more than a handful of development teams, you already have one. It's just accidental, inconsistent, and probably frustrating.
The real question is: Will you intentionally design a platform that accelerates your teams, or continue letting each squad reinvent wheels in isolation?
What's the most duplicated effort across your development teams right now? What would happen if you solved it once, elegantly, and made it available to everyone?
Share this article
Related articles

The End of Phishing: Passkeys Make Lookalike Sites Useless
A passkey registered on bank.com will not activate on bank-secure-login.com. The cryptography enforces the boundary without asking the user to spot the fake. This post covers why traditional MFA keeps failing, what the 2025 adoption numbers actually say, and how to run a working passkey demo on your own machine in 20 minutes.

Sandboxed Agents: Giving Your Code Monkeys Their Own Sandbox
Coding agents that can delete your work, mine cryptocurrency, and exfiltrate data are not hypothetical. This post covers how sandboxed execution works, which isolation technologies to choose for your threat model, and how to build a working Docker-based sandbox from scratch.

Spec-Driven Development: Let AI Read the Boring Stuff For You
Most teams consult specifications when things break. Spec-driven development turns that around, making the spec the source of truth before a single line of code is written. This post covers the four pillars of SDD, a step-by-step Claude Code walkthrough using FHIR validation, the current tooling landscape, and an honest look at where the practice still falls short.
Enjoyed this article?
Subscribe to get more insights delivered to your inbox monthly
Subscribe to Newsletter