---
title: "Building an Internal Developer Platform on Backstage and GitOps"
lang: "en"
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/post/internal-developer-platform-backstage-gitops
---

![Blog post image for Building an Internal Developer Platform on Backstage and GitOps - How golden paths in Backstage, self-service software templates, and Argo CD let product teams create, build, and ship services without filing tickets to the platform team.](/_astro/hero.Dq3xrist_Z26xY6v.webp)

[Home](/)›[Case studies](/case-studies)›[All Categories](/case-studies/categories)›[DevOps](/case-studies/categories/devops)

Case studies

[Prev in DevOpsCutting a SaaS AWS Bill 41% Without Slowing Delivery](/case-studies/post/aws-cost-optimization-saas-case-study)[Next in DevOpsMigrating a Monolith to Kubernetes Without a Big-Bang Cutover](/case-studies/post/monolith-to-kubernetes-strangler-migration)

[DevOps](/case-studies/categories/devops)[Platform Engineering](/case-studies/categories/platform-engineering)[Cloud Native](/case-studies/categories/cloud-native)

# Building an Internal Developer Platform on Backstage and GitOps

[Mohammad Abu Mattar](/authors/mohammad-abu-mattar)Published: 03 Aug 2026Updated: 03 Aug 202609 Mins read13 Mins listen

[Markdown for AI(opens in a new tab)](/post/internal-developer-platform-backstage-gitops/index.md "Open the plain-Markdown version of this page, for pasting into an AI tool")

TL;DR

How golden paths in Backstage, self-service software templates, and Argo CD let product teams create, build, and ship services without filing tickets to the platform team.

Series

[Platform Engineering](/series/platform-engineering)1/1

All posts in this series (1)

Case Studies1

1.  [Building an Internal Developer Platform on Backstage and GitOpsYou are here](/case-studies/post/internal-developer-platform-backstage-gitops)

### Building an Internal Developer Platform on Backstage and GitOps

Contents

[Impact](#impact)[The problem](#the-problem)[Constraints](#constraints)[Architecture](#architecture)[Implementation](#implementation)[Results](#results)[Lessons](#lessons)[Frequently Asked Questions](#frequently-asked-questions)[References](#references)

Product teams were spending more time waiting on the platform team than building features. Spinning up a new service meant opening a ticket and waiting for someone to provision a repo, wire up CI, write Kubernetes manifests, and hook up deployment. Each of those handoffs added days. We built an internal developer platform that turned that whole sequence into a self-service golden path: Backstage for the portal and software templates, Git as the single source of truth, and Argo CD reconciling desired state into the clusters. A developer picks a template, fills a short form, and gets a working repo plus a running service, with policy and RBAC acting as guardrails rather than manual gates.

## [Impact](#impact)

The headline change was that creating and shipping a service stopped being a ticket and became a form. New services that used to take teams the better part of a sprint to stand up now scaffold in minutes, and the first deploy happens on merge without anyone from the platform team touching it. The platform team moved from doing one-off deploys to maintaining the templates that everyone else uses.

Adoption is the metric that actually matters here, because a platform nobody uses is just more software to run. Within the first quarter most new services were created through the golden path rather than by hand, which is the signal that the paved road was genuinely easier than going around it. These numbers are specific to this rollout and were measured on our own usage, so treat them as a shape to expect rather than a guarantee.

0%+new services created via the golden path (measured)

0tickets to create and deploy a new service

0 PRfrom template to running in dev

## [The problem](#the-problem)

Every new service started the same way: a ticket. The platform team owned the repo templates, the CI config, the base Kubernetes manifests, and the deploy pipeline, so nothing shipped without them in the loop. That made sense when there were a handful of services, but it stopped scaling. The queue grew, context-switching killed the platform team’s own roadmap, and product teams learned to batch requests, which made each one bigger and slower.

The deeper issue was that knowledge lived in people’s heads and in copy-pasted YAML. Two teams standing up similar services would end up with subtly different setups, because each one copied whatever the last project happened to do. There was no paved road, just a lot of dirt tracks that mostly worked. When something went wrong in one of those setups, debugging it meant reverse-engineering choices nobody remembered making.

We wanted product teams to move without asking permission for routine work, while the platform team kept ownership of what “correct” looks like. That is the tension an internal developer platform exists to resolve.

## [Constraints](#constraints)

Worth knowing

The platform had to satisfy a few hard constraints, and every design decision came back to them.

-   **Self-service by default.** The common case, creating and deploying a service, had to happen with zero tickets and no human in the platform team’s loop.
-   **Git as the source of truth.** Every change to what runs in a cluster had to be a commit, so we get review, history, and a trivial rollback for free. No `kubectl apply` from laptops.
-   **Guardrails, not gates.** Policy and RBAC had to be enforced automatically. A human manually approving routine deploys would just recreate the ticket queue we were killing.
-   **Paved road, not a walled garden.** Teams with genuinely unusual needs had to be able to step off the golden path without the platform blocking them, as long as they still passed policy.

## [Architecture](#architecture)

The platform is three moving parts wired together by Git. Backstage is the front door, where developers discover services and kick off golden paths. Git holds both application code and the deployment config that describes desired state. Argo CD watches Git and reconciles that desired state into the Kubernetes clusters. Backstage never talks to the clusters to make changes; it only ever writes to Git, which keeps the whole system auditable.

Backstage scaffolds into Git, CI builds and signs the image and writes its digest back to Git, and Argo CD reconciles that desired state into the clusters. Policy admits or denies at the cluster edge.

The Backstage catalog models the world as a small set of entities, and understanding those makes the rest of the platform click. A `Template` describes a golden path: its input parameters as a JSON schema, and the steps it runs to scaffold a service. Each template produces a `Component`, which is an actual service owned by a `Group` (a team). Argo CD then manages an `Application` resource that points at the component’s config in Git and syncs it to a cluster.

A Template scaffolds Components, each owned by a Group. An Argo CD Application points at a Component's config in Git and defines where and how it deploys.

The reason Git sits in the middle of everything is that it turns two hard problems, auditability and rollback, into one solved problem: version control. Every deploy is a diff you can read, and undoing a bad change is `git revert`, which Argo CD then reconciles back automatically.

## [Implementation](#implementation)

The heart of the platform is the scaffolding flow. When a developer picks a template and submits the form, Backstage’s scaffolder renders a skeleton from the template’s inputs, creates a repository, opens a pull request, and registers the new component in the catalog. That is the moment the ticket used to be filed; now it is a button.

Developer fills a template form, Backstage scaffolds the repo and registers the component, CI builds and signs the image, the digest lands in the config repo, and Argo CD syncs it to the cluster.

A software template is a `Template` entity plus a skeleton directory. The parameters block is a JSON schema, so Backstage renders it as a validated form for free. The steps block is what runs when the form is submitted.

template.yaml (Backstage software template)

```
1apiVersion: scaffolder.backstage.io/v1beta32kind: Template3metadata:4  name: node-service5  title: Node.js service (golden path)6  description: A production-ready Node.js service with CI, Helm, and GitOps wired up.7spec:8  owner: group:platform9  type: service10  parameters:11    - title: Service details12      required: [name, owner]13      properties:14        name:15          title: Name16          type: string17          pattern: '^[a-z][a-z0-9-]{2,30}$'18        owner:19          title: Owning team20          type: string21          ui:field: OwnerPicker22  steps:23    - id: fetch24      name: Fetch skeleton25      action: fetch:template26      input:27        url: ./skeleton28        values:29          name: ${{ parameters.name }}30          owner: ${{ parameters.owner }}31    - id: publish32      name: Create repository33      action: publish:github34      input:35        repoUrl: github.com?owner=acme&repo=${{ parameters.name }}36        defaultBranch: main37    - id: register38      name: Register in catalog39      action: catalog:register40      input:41        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}42        catalogInfoPath: /catalog-info.yaml
```

The skeleton ships the boring, correct defaults so no team has to reinvent them: a `catalog-info.yaml` so the service shows up in the catalog, a CI workflow that builds and signs the container image, a Helm chart, and the Argo CD `Application` that ties it to a cluster. That last file is what turns a repo into something GitOps actually deploys.

argocd-application.yaml (scaffolded into the config repo)

```
1apiVersion: argoproj.io/v1alpha12kind: Application3metadata:4  name: node-service-dev5  namespace: argocd6spec:7  project: default8  source:9    repoURL: https://github.com/acme/config10    path: apps/node-service/dev11    targetRevision: main12  destination:13    server: https://kubernetes.default.svc14    namespace: node-service15  syncPolicy:16    automated:17      prune: true18      selfHeal: true
```

Because the deploy config lives in Git and Argo CD self-heals, the platform naturally behaves like a state machine. A service moves from scaffolded, to built, to deployed in dev, through a policy gate, and on to production, and every transition is a commit. Modeling it that way made it obvious where the guardrails belong.

A service moves from scaffolded to running in prod through discrete, Git-driven transitions. A policy violation routes to a blocked state that clears once the config is fixed and re-synced.

Guardrails are enforced at two layers. RBAC in Backstage and in the clusters decides who can do what, and admission policy with OPA or Kyverno decides what is allowed to run at all. A policy that every workload must set resource limits, for example, is a Kyverno rule that rejects the deploy at admission rather than a checklist item in a review.

require-resource-limits.yaml (Kyverno policy)

```
1apiVersion: kyverno.io/v12kind: ClusterPolicy3metadata:4  name: require-resource-limits5spec:6  validationFailureAction: Enforce7  rules:8    - name: check-limits9      match:10        any:11          - resources:12              kinds: ['Pod']13      validate:14        message: 'CPU and memory limits are required.'15        pattern:16          spec:17            containers:18              - resources:19                  limits:20                    memory: '?*'21                    cpu: '?*'
```

The repo layout keeps application code and deployment config separate, which is a deliberate GitOps choice: app repos change on every feature, config repos change on every deploy, and keeping them apart makes the deploy history readable.

-   Directoryconfig/
    
    -   Directoryapps/
        
        -   Directorynode-service/
            
            -   Directorydev/
                
                -   kustomization.yaml
                -   deployment.yaml
                
            -   Directoryprod/
                
                -   kustomization.yaml
                -   deployment.yaml
                
            
        
    -   Directoryargocd/
        
        -   node-service-dev.yaml
        -   node-service-prod.yaml
        
    

Onboarding an existing service that predates the platform is a short runbook rather than a rebuild.

1.  Add a `catalog-info.yaml` to the repo so Backstage discovers and indexes the service.
2.  Move its deployment manifests into the config repo under a per-environment path.
3.  Add an Argo CD `Application` pointing at that path, starting with automated sync disabled.
4.  Compare the live cluster state against Git until the diff is clean, then enable automated sync and self-heal.

## [Results](#results)

The change people felt first was speed. Creating a new service went from a multi-day, ticket-driven sequence to a form that produces a working repo and a service running in the dev cluster off a single merge. The first deploy happens with no platform-team involvement, which is the whole point. Read and write access to what runs is governed by RBAC and policy, so faster did not mean looser.

Adoption is the result that tells you the platform actually worked, and it climbed quickly once the golden path was clearly easier than the old dirt tracks. Within the first quarter the large majority of new services came through templates rather than by hand. The platform team’s own time shifted from doing one-off deploys to improving templates and policy, which compounds: one improvement to a template lands in every service scaffolded after it. These numbers are specific to this rollout and were measured on our own usage; treat them as a shape to expect, not a guarantee.

The other measurable win was consistency. Because every service starts from the same skeleton, the drift between projects that used to make debugging miserable mostly disappeared. When we needed to roll out a change like a new required label or a security default, we updated the template and the policy, and the fleet converged instead of needing a hand-edit per repo.

## [Lessons](#lessons)

The most important lesson is that a platform lives or dies by adoption, and adoption is earned by making the paved road genuinely faster than going around it. We resisted the urge to mandate the platform early. Instead we made the golden path the path of least resistance, and teams chose it. A mandate on a platform people dislike just produces malicious compliance.

Guardrails have to be automatic to matter. The first time we let a “quick manual approval” creep into a deploy path, we had reinvented the ticket queue in miniature. Encoding the rule as admission policy, so the platform enforces it without a human, is what kept self-service actually self-service.

Finally, treat the golden path as a product with a small number of well-maintained templates, not a template for every conceivable variation. A handful of paths that cover the common cases well beats a sprawling catalog nobody trusts. Teams with unusual needs step off the road and still pass policy, and that is fine. The goal was never to control every service, only to make the right thing the easy thing.

## [Frequently Asked Questions](#frequently-asked-questions)

A golden path is the supported, opinionated way to do a common task, like creating a new service, with the boring correct defaults already wired in. In this platform it is a Backstage software template that scaffolds a repo, CI, a Helm chart, and the GitOps config in one step. It is a paved road you are free to leave, not a wall you cannot cross.

Because Git turns auditability and rollback into a solved problem. Every deploy is a reviewable diff with history, and undoing a bad change is a revert that Argo CD reconciles automatically. If Backstage pushed changes directly to clusters, you would lose that trail and have to build approval and rollback yourself.

They are enforced by machines, not people. RBAC decides who can act, and admission policy with OPA or Kyverno decides what is allowed to run, both automatically at deploy time. There is no human in the routine path clicking approve, which is exactly the bottleneck a manual gate would recreate.

They step off the golden path. The platform does not block a team from writing their own manifests or CI, as long as the result still passes policy at admission. The golden path is the default that covers the common cases well, not a hard requirement for every service.

Add a catalog-info.yaml so Backstage indexes the service, move its manifests into the config repo, and add an Argo CD Application with automated sync off at first. Once the live state matches Git with a clean diff, turn on automated sync and self-heal. It is an adoption runbook, not a rebuild.

Adoption, specifically the share of new services created through the golden path rather than by hand. A platform nobody uses is just more software to operate. If teams choose the paved road on their own, it means the road is genuinely faster and safer than the alternative, which is the whole point.

No. Backstage and GitOps patterns apply to plenty of deployment targets. Kubernetes happens to pair well with Argo CD’s reconcile loop and with admission policy, which is why this platform uses it, but the core idea of self-service scaffolding into Git as the source of truth is portable.

## [References](#references)

-   [Backstage: Software Templates](https://backstage.io/docs/features/software-templates/)
-   [Backstage: Software Catalog](https://backstage.io/docs/features/software-catalog/)
-   [Argo CD: Declarative GitOps CD for Kubernetes](https://argo-cd.readthedocs.io/en/stable/)
-   [Argo CD: Application resource specification](https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/)
-   [Kyverno: Kubernetes-native policy management](https://kyverno.io/docs/)
-   [Open Policy Agent (OPA)](https://www.openpolicyagent.org/docs/latest/)
-   [Team Topologies: platform as a product](https://teamtopologies.com/key-concepts)

Was this useful?

## Tags

[#Backstage](/case-studies/tags/backstage)[#GitOps](/case-studies/tags/gitops)[#Argo CD](/case-studies/tags/argo-cd)[#Internal Developer Platform](/case-studies/tags/internal-developer-platform)[#Kubernetes](/case-studies/tags/kubernetes)[#Golden Paths](/case-studies/tags/golden-paths)[#Self Service](/case-studies/tags/self-service)

## Share

[Facebook](https://facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops "Share on Facebook")[Twitter](https://twitter.com/intent/tweet/?text=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps&url=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops "Share on Twitter")[LinkedIn](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops&title=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps&summary=How%20golden%20paths%20in%20Backstage%2C%20self-service%20software%20templates%2C%20and%20Argo%20CD%20let%20product%20teams%20create%2C%20build%2C%20and%20ship%20services%20without%20filing%20tickets%20to%20the%20platform%20team.&source=https://mkabumattar.com "Share on LinkedIn")[WhatsApp](https://wa.me/?text=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps%20https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops "Share on WhatsApp")[Telegram](https://t.me/share/url?url=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops&text=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps "Share on Telegram")[Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops&title=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps "Share on Reddit")[Hacker News](http://news.ycombinator.com/submitlink?u=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops&t=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps "Share on Hacker News")[Pinterest](https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops&media=&description=How%20golden%20paths%20in%20Backstage%2C%20self-service%20software%20templates%2C%20and%20Argo%20CD%20let%20product%20teams%20create%2C%20build%2C%20and%20ship%20services%20without%20filing%20tickets%20to%20the%20platform%20team. "Share on Pinterest")[Email](<mailto:?subject=Building%20an%20Internal%20Developer%20Platform%20on%20Backstage%20and%20GitOps&body=Check out this article: https%3A%2F%2Fmkabumattar.com%2Fcase-studies%2Fpost%2Finternal-developer-platform-backstage-gitops>)

## Comments

## You might also enjoy

More posts on similar topics

[![Cutting a SaaS AWS Bill 41% Without Slowing Delivery](/_astro/hero.DJTB593d_Z1iASgs.webp)](/case-studies/post/aws-cost-optimization-saas-case-study)

## [Cutting a SaaS AWS Bill 41% Without Slowing Delivery](/case-studies/post/aws-cost-optimization-saas-case-study)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Cloud Computing](/case-studies/categories/cloud-computing)
-   [DevOps](/case-studies/categories/devops)
-   [Cloud Native](/case-studies/categories/cloud-native)

A growing SaaS ran on EKS with a full GitOps pipeline, and it was over its AWS budget nearly every month. The reflex from leadership was the usual one: freeze features until the bill comes down. That

[#AWS](/case-studies/tags/aws)[#EKS](/case-studies/tags/eks)[#Kubernetes](/case-studies/tags/kubernetes)+7 tags

[read more](/case-studies/post/aws-cost-optimization-saas-case-study)

[![Migrating a Monolith to Kubernetes Without a Big-Bang Cutover](/_astro/hero.CAKh7bXG_Z1PnEBt.webp)](/case-studies/post/monolith-to-kubernetes-strangler-migration)

## [Migrating a Monolith to Kubernetes Without a Big-Bang Cutover](/case-studies/post/monolith-to-kubernetes-strangler-migration)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps](/case-studies/categories/devops)
-   [Cloud Native](/case-studies/categories/cloud-native)
-   [Architecture](/case-studies/categories/architecture)

Almost every failed "let's move off the monolith" project shares one detail: the plan was a big-bang cutover. Rewrite in parallel, pick a weekend, flip the switch, and pray. This is the opposite of th

[#Kubernetes](/case-studies/tags/kubernetes)[#EKS](/case-studies/tags/eks)[#Migration](/case-studies/tags/migration)+4 tags

[read more](/case-studies/post/monolith-to-kubernetes-strangler-migration)

[![QuenchWorks: Building a 0-CVE Container Image and Helm Chart Catalog](/_astro/hero.BfjMKoMg_ZI2zvl.webp)](/case-studies/post/quenchworks-zero-cve-catalog)

## [QuenchWorks: Building a 0-CVE Container Image and Helm Chart Catalog](/case-studies/post/quenchworks-zero-cve-catalog)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Security](/case-studies/categories/security)
-   [DevOps](/case-studies/categories/devops)
-   [Cloud Native](/case-studies/categories/cloud-native)

When Bitnami moved its long-trusted catalog behind a paid tier, thousands of teams woke up to a supply-chain problem they didn't choose. The free images they had pinned in production would stop gettin

[#Containers](/case-studies/tags/containers)[#Wolfi](/case-studies/tags/wolfi)[#Helm](/case-studies/tags/helm)+5 tags

[read more](/case-studies/post/quenchworks-zero-cve-catalog)

[![Zero-Downtime PostgreSQL Major-Version Upgrade at Scale](/_astro/hero.C03RcOLI_141vsK.webp)](/case-studies/post/zero-downtime-postgres-upgrade)

## [Zero-Downtime PostgreSQL Major-Version Upgrade at Scale](/case-studies/post/zero-downtime-postgres-upgrade)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps](/case-studies/categories/devops)
-   [Databases](/case-studies/categories/databases)
-   [Cloud Computing](/case-studies/categories/cloud-computing)

A multi-terabyte PostgreSQL 12 database was reaching end of life, and the business ran around the clock, so the usual answer of "schedule a maintenance window" was off the table. We upgraded it to Pos

[#PostgreSQL](/case-studies/tags/postgresql)[#Logical Replication](/case-studies/tags/logical-replication)[#Zero Downtime](/case-studies/tags/zero-downtime)+3 tags

[read more](/case-studies/post/zero-downtime-postgres-upgrade)

[![Multi-Region Active-Active for a Payments API](/_astro/hero.RPbRRCdE_flG0g.webp)](/case-studies/post/multi-region-active-active-payments)

## [Multi-Region Active-Active for a Payments API](/case-studies/post/multi-region-active-active-payments)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Architecture](/case-studies/categories/architecture)
-   [Cloud Computing](/case-studies/categories/cloud-computing)
-   [Reliability](/case-studies/categories/reliability)

A payments API that moves real money had been running comfortably in a single AWS region for years. It was reliable until the day it was not: a regional control-plane incident took the whole service o

[#Multi Region](/case-studies/tags/multi-region)[#Active Active](/case-studies/tags/active-active)[#Payments](/case-studies/tags/payments)+5 tags

[read more](/case-studies/post/multi-region-active-active-payments)

5 related posts
