{"schemaVersion":1,"id":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port","title":"Platform Engineering: Building Internal Developer Platforms with Backstage and Port","description":"A hands-on guide to building IDPs that developers actually want to use, covering Backstage plugins, Port blueprints, golden paths, and the developer experience metrics that matter.","language":"en","datePublished":"2025-12-27","dateModified":null,"contentFormat":"text/markdown","content":"DevOps\n\n# Platform Engineering: Building Internal Developer Platforms with Backstage and Port\n\n2025-12-278 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/platform-engineering-idp-backstage-port>)\n\n## The State of Internal Developer Tooling\n\nInternal tooling at most companies consists of incomplete Bash scripts and Confluence pages that have not been updated since 2023. Developers avoid the \"official\" workflows because those workflows were designed by someone who left eighteen months ago, and the support Slack channel has three members, two of which are bots.\n\nPlatform engineering is not about building another tool. It is about building a product, one where the customers happen to sit in the same office (or Zoom call) as the platform team.\n\n## Backstage: The Open-Source Framework\n\nSpotify open-sourced [Backstage](<https://backstage.io/>) in 2020, and it has grown into the de facto framework for building developer portals. Backstage out of the box is a service catalog with a well-designed UI. The real power comes from plugins, and that is where the engineering investment lies.\n\n### Setting Up a Backstage Instance\n\nThe ` npx @backstage/create-app ` setup can be skipped here, the documentation covers it adequately. What it does not cover well is how to structure an instance for a real organization.\n\n```yaml\n# app-config.yaml - production-grade configuration\napp:\n title: \"Acme Developer Portal\"\n baseUrl: https://portal.acme.internal\n\nbackend:\n baseUrl: https://portal.acme.internal\n database:\n client: pg\n connection:\n host: ${POSTGRES_HOST}\n port: ${POSTGRES_PORT}\n user: ${POSTGRES_USER}\n password: ${POSTGRES_PASSWORD}\n\ncatalog:\n rules:\n - allow: [Component, System, API, Resource, Location, Template, Domain, Group, User]\n providers:\n github:\n acmeOrg:\n organization: 'acme-corp'\n catalogPath: '/catalog-info.yaml'\n filters:\n repository: '.*'\n schedule:\n frequency: { minutes: 30 }\n timeout: { minutes: 3 }\n\n locations:\n - type: url\n target: https://github.com/acme-corp/backstage-catalog/blob/main/org.yaml\n```\n\nThe critical piece here is the GitHub discovery provider. Instead of manually registering every service, you let Backstage crawl your GitHub org and find ` catalog-info.yaml ` files. This is the only approach that scales past twenty services.\n\n### A Production catalog-info.yaml\n\nMost examples show a minimal spec. Here is what a production catalog-info.yaml looks like after six months of running Backstage, once the relevant metadata requirements become clear:\n\n```yaml\napiVersion: backstage.io/v1alpha1\nkind: Component\nmetadata:\n name: payment-service\n description: \"Handles payment processing, refunds, and subscription billing\"\n annotations:\n github.com/project-slug: acme-corp/payment-service\n backstage.io/techdocs-ref: dir:.\n pagerduty.com/service-id: P1A2B3C\n sonarqube.org/project-key: acme_payment-service\n backstage.io/kubernetes-id: payment-service\n jenkins.io/job-full-name: acme/payment-service/main\n tags:\n - java\n - payments\n - pci\n links:\n - url: https://grafana.acme.internal/d/payment-svc\n title: Grafana Dashboard\n icon: dashboard\n - url: https://runbooks.acme.internal/payment-service\n title: Runbook\n icon: docs\nspec:\n type: service\n lifecycle: production\n owner: team-payments\n system: billing-platform\n dependsOn:\n - resource:default/payments-db\n - component:default/notification-service\n providesApis:\n - payment-api\n consumesApis:\n - stripe-gateway-api\n```\n\nThe ` annotations ` block is the key integration point. Each annotation connects Backstage to an external system, PagerDuty for on-call, SonarQube for code quality, Kubernetes for runtime status. Without these, a developer portal provides little value beyond a spreadsheet.\n\n## Port: The Declarative Alternative\n\n[Port](<https://www.getport.io/>) takes a fundamentally different approach. Instead of plugins and code, you define your data model as blueprints, essentially schemas for the entities in your developer platform.\n\n```json\n{\n \"identifier\": \"microservice\",\n \"title\": \"Microservice\",\n \"icon\": \"Microservice\",\n \"schema\": {\n \"properties\": {\n \"language\": {\n \"type\": \"string\",\n \"enum\": [\"Go\", \"Java\", \"Python\", \"Node.js\", \"Rust\"],\n \"title\": \"Language\"\n },\n \"tier\": {\n \"type\": \"string\",\n \"enum\": [\"critical\", \"standard\", \"experimental\"],\n \"title\": \"Service Tier\",\n \"description\": \"Determines SLO targets and on-call requirements\"\n },\n \"repository_url\": {\n \"type\": \"string\",\n \"format\": \"url\",\n \"title\": \"Repository\"\n },\n \"last_deploy\": {\n \"type\": \"string\",\n \"format\": \"date-time\",\n \"title\": \"Last Deployment\"\n },\n \"dora_lead_time_hours\": {\n \"type\": \"number\",\n \"title\": \"Lead Time (hours)\"\n },\n \"resource_cost_monthly\": {\n \"type\": \"number\",\n \"title\": \"Monthly Cost ($)\"\n }\n },\n \"required\": [\"language\", \"tier\", \"repository_url\"]\n },\n \"relations\": {\n \"team\": {\n \"target\": \"team\",\n \"required\": true,\n \"title\": \"Owning Team\"\n },\n \"environment\": {\n \"target\": \"environment\",\n \"many\": true,\n \"title\": \"Deployed Environments\"\n }\n }\n}\n```\n\nA genuine strength of Port is the ability to model things that Backstage makes awkward. Cost attribution, compliance status, DORA metrics, these become first-class properties instead of annotations you have to parse.\n\n### Self-Service Actions in Port\n\nThis is where Port shines. You can create self-service actions that trigger real infrastructure changes:\n\n```json\n{\n \"identifier\": \"provision_database\",\n \"title\": \"Provision PostgreSQL Database\",\n \"trigger\": \"CREATE\",\n \"invocationMethod\": {\n \"type\": \"GITHUB\",\n \"org\": \"acme-corp\",\n \"repo\": \"platform-actions\",\n \"workflow\": \"provision-db.yml\"\n },\n \"userInputs\": {\n \"properties\": {\n \"db_name\": {\n \"type\": \"string\",\n \"pattern\": \"^[a-z][a-z0-9_]{2,30}$\",\n \"title\": \"Database Name\"\n },\n \"instance_size\": {\n \"type\": \"string\",\n \"enum\": [\"small\", \"medium\", \"large\"],\n \"default\": \"small\",\n \"title\": \"Instance Size\"\n },\n \"enable_read_replica\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"title\": \"Enable Read Replica\"\n }\n },\n \"required\": [\"db_name\", \"instance_size\"]\n }\n}\n```\n\nThe developer clicks a button, fills in a form, and a GitHub Actions workflow provisions a real RDS instance with Terraform. No tickets, no waiting for the infra team, no Slack DMs at 4pm on a Friday.\n\n## Golden Paths: The Heart of Platform Engineering\n\nA golden path is a pre-paved, opinionated way to do something. It's not the only way, it's the recommended way. The distinction matters.\n\nThe following Backstage Software Template implements a golden path for creating a new microservice:\n\n```yaml\napiVersion: scaffolder.backstage.io/v1beta3\nkind: Template\nmetadata:\n name: go-microservice\n title: Go Microservice (Golden Path)\n description: \"Production-ready Go service with CI/CD, observability, and Kubernetes manifests\"\n tags:\n - go\n - recommended\n - golden-path\nspec:\n owner: team-platform\n type: service\n parameters:\n - title: Service Details\n required:\n - name\n - owner\n - tier\n properties:\n name:\n title: Service Name\n type: string\n pattern: '^[a-z][a-z0-9-]{2,30}$'\n ui:help: 'Lowercase, hyphens only. This becomes the repo name, k8s namespace, and DNS entry.'\n owner:\n title: Owning Team\n type: string\n ui:field: OwnerPicker\n ui:options:\n catalogFilter:\n kind: Group\n tier:\n title: Service Tier\n type: string\n enum: ['critical', 'standard', 'experimental']\n enumNames: ['Critical (99.9% SLO)', 'Standard (99.5% SLO)', 'Experimental (best-effort)']\n - title: Infrastructure\n properties:\n needsDatabase:\n title: PostgreSQL Database\n type: boolean\n default: false\n needsRedis:\n title: Redis Cache\n type: boolean\n default: false\n needsQueue:\n title: SQS Queue\n type: boolean\n default: false\n steps:\n - id: fetch\n name: Fetch Template\n action: fetch:template\n input:\n url: ./skeleton\n values:\n name: ${{ parameters.name }}\n owner: ${{ parameters.owner }}\n tier: ${{ parameters.tier }}\n - id: publish\n name: Create Repository\n action: publish:github\n input:\n allowedHosts: ['github.com']\n repoUrl: 'github.com?owner=acme-corp&repo=${{ parameters.name }}'\n defaultBranch: main\n protectDefaultBranch: true\n requireCodeOwnerReviews: true\n - id: register\n name: Register in Catalog\n action: catalog:register\n input:\n repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}\n catalogInfoPath: '/catalog-info.yaml'\n```\n\nIn under two minutes, a developer goes from \"I need a new service\" to having a repository with CI/CD pipelines, Kubernetes manifests, a Dockerfile, structured logging, health checks, Prometheus metrics, and a Grafana dashboard, all pre-configured and following organizational standards.\n\n## Developer Experience Metrics Worth Tracking\n\nImprovement requires measurement, and most platform teams measure the wrong things. \"Number of Backstage page views\" provides no actionable insight. The following metrics are worth tracking:\n\n```python\n# developer_experience_metrics.py\nfrom dataclasses import dataclass\nfrom datetime import timedelta\n\n@dataclass\nclass PlatformMetrics:\n # Time from \"I need X\" to \"X is provisioned and usable\"\n time_to_first_request: timedelta\n\n # Percentage of new services using golden path templates\n golden_path_adoption_rate: float\n\n # How many times developers had to ask for help on Slack\n # vs self-served through the portal\n self_service_ratio: float\n\n # DORA: time from commit to production\n lead_time_for_changes: timedelta\n\n # How often developers abandon a self-service flow midway\n action_abandonment_rate: float\n\n # Net Promoter Score from quarterly developer surveys\n developer_nps: int\n\n# Real numbers from a client engagement (anonymized)\nbefore_idp = PlatformMetrics(\n time_to_first_request=timedelta(days=5),\n golden_path_adoption_rate=0.0,\n self_service_ratio=0.12,\n lead_time_for_changes=timedelta(days=14),\n action_abandonment_rate=0.0, # no self-service existed\n developer_nps=-23\n)\n\nafter_idp_6_months = PlatformMetrics(\n time_to_first_request=timedelta(minutes=8),\n golden_path_adoption_rate=0.73,\n self_service_ratio=0.81,\n lead_time_for_changes=timedelta(hours=4),\n action_abandonment_rate=0.15,\n developer_nps=41\n)\n```\n\nThe before/after numbers from that engagement are striking. Five days to eight minutes for provisioning. But the metric that convinced leadership was the NPS swing, from -23 to +41. Developers went from actively hostile about internal tools to recommending the platform to new hires.\n\n## Where It Falls Apart\n\nNot everything is smooth. Here are the common pitfalls:\n\n**Backstage plugin quality is wildly inconsistent.** Some community plugins are excellent. Others haven't been updated in a year and break on every Backstage upgrade. Budget time for plugin maintenance or build your own.\n\n**Port's pricing scales with entities.** When you start modeling every Kubernetes pod, every Lambda function, every S3 bucket as an entity, the bill grows fast. Be deliberate about what you model.\n\n**The hardest part isn't technical.** It's convincing teams to register their services in the catalog and keep metadata up to date. A proven solution is a GitHub Action that fails CI if ` catalog-info.yaml ` is missing or has stale ownership info:\n\n```yaml\n# .github/workflows/catalog-lint.yml\nname: Catalog Info Lint\non: [pull_request]\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Validate catalog-info.yaml\n run: |\n if [ ! -f catalog-info.yaml ]; then\n echo \"::error::Missing catalog-info.yaml. See https://portal.acme.internal/docs/catalog-requirements\"\n exit 1\n fi\n # Validate owner exists in our org\n OWNER=$(yq '.spec.owner' catalog-info.yaml)\n if ! curl -sf \"https://portal.acme.internal/api/catalog/entities/by-name/group/default/${OWNER}\" > /dev/null; then\n echo \"::error::Owner '${OWNER}' not found in Backstage catalog. Update spec.owner.\"\n exit 1\n fi\n```\n\n## Recommendation\n\nFor teams of under 100 developers wanting results in weeks, Port is the stronger starting point. The blueprint model gets you a working IDP fast, and the self-service actions are production-ready out of the box.\n\nFor organizations over 100 developers, or those needing deep customization or with strong opinions about data ownership, Backstage is the better fit. But commitment is essential, a half-built Backstage instance is worse than no developer portal at all.\n\nEither way, start with the service catalog. Get ownership right. Then add golden paths. Then self-service actions. Trying to do all three at once is how platform teams burn out and get dismantled.\n\nThis pattern has played out repeatedly across organizations. The teams that succeed treat the platform as a product with real users, real feedback loops, and real roadmaps. The teams that fail treat it as a project with a deadline and a handoff. There is no handoff. The platform is never done.\n\n## Frequently asked questions\n\n### What's the difference between Backstage and Port?\n\nBackstage is an open-source framework you host and extend with plugins. Port is a SaaS platform with a declarative blueprint model. Backstage gives you full control but demands engineering investment; Port gets you running faster but with less customization depth.\n\n### How many engineers do I need to maintain an IDP built on Backstage?\n\nRealistically, plan for 1-2 dedicated platform engineers per 50-80 application developers. That ratio drops with Port since they handle the infrastructure, but you still need someone owning blueprints and integrations.\n\n### Are golden paths mandatory for all teams?\n\nNo. Golden paths should be the path of least resistance, not a mandate. Teams that need to deviate should be able to, but the friction should be intentional and documented.\n\n## Tips\n\n1. **Tip:** Always start your IDP journey by measuring ` DORA metrics ` before building anything, you need a baseline or you're just guessing.\n2. **Tip:** Use Backstage's ` catalog-info.yaml ` as the single source of truth for service ownership. If it's not in the catalog, it doesn't exist.\n3. **Tip:** Golden paths should have escape hatches. If a developer *must* deviate, make the non-golden path visible but not blocked, ` softGate: true ` in your Port blueprint.\n\n- platform-engineering\n- backstage\n- port\n- idp\n- developer-experience\n- golden-paths\n- self-service\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Platform Engineering: Building Internal Developer Platforms with Backstage and Port","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#title"},{"level":2,"text":"The State of Internal Developer Tooling","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#the-state-of-internal-developer-tooling"},{"level":2,"text":"Backstage: The Open-Source Framework","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#backstage-the-open-source-framework"},{"level":3,"text":"Setting Up a Backstage Instance","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#setting-up-a-backstage-instance"},{"level":3,"text":"A Production catalog-info.yaml","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#a-production-catalog-infoyaml"},{"level":2,"text":"Port: The Declarative Alternative","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#port-the-declarative-alternative"},{"level":3,"text":"Self-Service Actions in Port","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#self-service-actions-in-port"},{"level":2,"text":"Golden Paths: The Heart of Platform Engineering","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#golden-paths-the-heart-of-platform-engineering"},{"level":2,"text":"Developer Experience Metrics Worth Tracking","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#developer-experience-metrics-worth-tracking"},{"level":2,"text":"Where It Falls Apart","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#where-it-falls-apart"},{"level":2,"text":"Recommendation","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#recommendation"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#questions"},{"level":3,"text":"What's the difference between Backstage and Port?","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#question-1"},{"level":3,"text":"How many engineers do I need to maintain an IDP built on Backstage?","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#question-2"},{"level":3,"text":"Are golden paths mandatory for all teams?","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#article-tips"}],"sections":[{"heading":"Platform Engineering: Building Internal Developer Platforms with Backstage and Port","level":1,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port","content":"DevOps"},{"heading":"Platform Engineering: Building Internal Developer Platforms with Backstage and Port","level":1,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#title","content":"2025-12-278 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/platform-engineering-idp-backstage-port>)"},{"heading":"The State of Internal Developer Tooling","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#the-state-of-internal-developer-tooling","content":"Internal tooling at most companies consists of incomplete Bash scripts and Confluence pages that have not been updated since 2023. Developers avoid the \"official\" workflows because those workflows were designed by someone who left eighteen months ago, and the support Slack channel has three members, two of which are bots.\n\nPlatform engineering is not about building another tool. It is about building a product, one where the customers happen to sit in the same office (or Zoom call) as the platform team."},{"heading":"Backstage: The Open-Source Framework","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#backstage-the-open-source-framework","content":"Spotify open-sourced [Backstage](<https://backstage.io/>) in 2020, and it has grown into the de facto framework for building developer portals. Backstage out of the box is a service catalog with a well-designed UI. The real power comes from plugins, and that is where the engineering investment lies."},{"heading":"Setting Up a Backstage Instance","level":3,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#setting-up-a-backstage-instance","content":"The ` npx @backstage/create-app ` setup can be skipped here, the documentation covers it adequately. What it does not cover well is how to structure an instance for a real organization.\n\n```yaml\n# app-config.yaml - production-grade configuration\napp:\n title: \"Acme Developer Portal\"\n baseUrl: https://portal.acme.internal\n\nbackend:\n baseUrl: https://portal.acme.internal\n database:\n client: pg\n connection:\n host: ${POSTGRES_HOST}\n port: ${POSTGRES_PORT}\n user: ${POSTGRES_USER}\n password: ${POSTGRES_PASSWORD}\n\ncatalog:\n rules:\n - allow: [Component, System, API, Resource, Location, Template, Domain, Group, User]\n providers:\n github:\n acmeOrg:\n organization: 'acme-corp'\n catalogPath: '/catalog-info.yaml'\n filters:\n repository: '.*'\n schedule:\n frequency: { minutes: 30 }\n timeout: { minutes: 3 }\n\n locations:\n - type: url\n target: https://github.com/acme-corp/backstage-catalog/blob/main/org.yaml\n```\n\nThe critical piece here is the GitHub discovery provider. Instead of manually registering every service, you let Backstage crawl your GitHub org and find ` catalog-info.yaml ` files. This is the only approach that scales past twenty services."},{"heading":"A Production catalog-info.yaml","level":3,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#a-production-catalog-infoyaml","content":"Most examples show a minimal spec. Here is what a production catalog-info.yaml looks like after six months of running Backstage, once the relevant metadata requirements become clear:\n\n```yaml\napiVersion: backstage.io/v1alpha1\nkind: Component\nmetadata:\n name: payment-service\n description: \"Handles payment processing, refunds, and subscription billing\"\n annotations:\n github.com/project-slug: acme-corp/payment-service\n backstage.io/techdocs-ref: dir:.\n pagerduty.com/service-id: P1A2B3C\n sonarqube.org/project-key: acme_payment-service\n backstage.io/kubernetes-id: payment-service\n jenkins.io/job-full-name: acme/payment-service/main\n tags:\n - java\n - payments\n - pci\n links:\n - url: https://grafana.acme.internal/d/payment-svc\n title: Grafana Dashboard\n icon: dashboard\n - url: https://runbooks.acme.internal/payment-service\n title: Runbook\n icon: docs\nspec:\n type: service\n lifecycle: production\n owner: team-payments\n system: billing-platform\n dependsOn:\n - resource:default/payments-db\n - component:default/notification-service\n providesApis:\n - payment-api\n consumesApis:\n - stripe-gateway-api\n```\n\nThe ` annotations ` block is the key integration point. Each annotation connects Backstage to an external system, PagerDuty for on-call, SonarQube for code quality, Kubernetes for runtime status. Without these, a developer portal provides little value beyond a spreadsheet."},{"heading":"Port: The Declarative Alternative","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#port-the-declarative-alternative","content":"[Port](<https://www.getport.io/>) takes a fundamentally different approach. Instead of plugins and code, you define your data model as blueprints, essentially schemas for the entities in your developer platform.\n\n```json\n{\n \"identifier\": \"microservice\",\n \"title\": \"Microservice\",\n \"icon\": \"Microservice\",\n \"schema\": {\n \"properties\": {\n \"language\": {\n \"type\": \"string\",\n \"enum\": [\"Go\", \"Java\", \"Python\", \"Node.js\", \"Rust\"],\n \"title\": \"Language\"\n },\n \"tier\": {\n \"type\": \"string\",\n \"enum\": [\"critical\", \"standard\", \"experimental\"],\n \"title\": \"Service Tier\",\n \"description\": \"Determines SLO targets and on-call requirements\"\n },\n \"repository_url\": {\n \"type\": \"string\",\n \"format\": \"url\",\n \"title\": \"Repository\"\n },\n \"last_deploy\": {\n \"type\": \"string\",\n \"format\": \"date-time\",\n \"title\": \"Last Deployment\"\n },\n \"dora_lead_time_hours\": {\n \"type\": \"number\",\n \"title\": \"Lead Time (hours)\"\n },\n \"resource_cost_monthly\": {\n \"type\": \"number\",\n \"title\": \"Monthly Cost ($)\"\n }\n },\n \"required\": [\"language\", \"tier\", \"repository_url\"]\n },\n \"relations\": {\n \"team\": {\n \"target\": \"team\",\n \"required\": true,\n \"title\": \"Owning Team\"\n },\n \"environment\": {\n \"target\": \"environment\",\n \"many\": true,\n \"title\": \"Deployed Environments\"\n }\n }\n}\n```\n\nA genuine strength of Port is the ability to model things that Backstage makes awkward. Cost attribution, compliance status, DORA metrics, these become first-class properties instead of annotations you have to parse."},{"heading":"Self-Service Actions in Port","level":3,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#self-service-actions-in-port","content":"This is where Port shines. You can create self-service actions that trigger real infrastructure changes:\n\n```json\n{\n \"identifier\": \"provision_database\",\n \"title\": \"Provision PostgreSQL Database\",\n \"trigger\": \"CREATE\",\n \"invocationMethod\": {\n \"type\": \"GITHUB\",\n \"org\": \"acme-corp\",\n \"repo\": \"platform-actions\",\n \"workflow\": \"provision-db.yml\"\n },\n \"userInputs\": {\n \"properties\": {\n \"db_name\": {\n \"type\": \"string\",\n \"pattern\": \"^[a-z][a-z0-9_]{2,30}$\",\n \"title\": \"Database Name\"\n },\n \"instance_size\": {\n \"type\": \"string\",\n \"enum\": [\"small\", \"medium\", \"large\"],\n \"default\": \"small\",\n \"title\": \"Instance Size\"\n },\n \"enable_read_replica\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"title\": \"Enable Read Replica\"\n }\n },\n \"required\": [\"db_name\", \"instance_size\"]\n }\n}\n```\n\nThe developer clicks a button, fills in a form, and a GitHub Actions workflow provisions a real RDS instance with Terraform. No tickets, no waiting for the infra team, no Slack DMs at 4pm on a Friday."},{"heading":"Golden Paths: The Heart of Platform Engineering","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#golden-paths-the-heart-of-platform-engineering","content":"A golden path is a pre-paved, opinionated way to do something. It's not the only way, it's the recommended way. The distinction matters.\n\nThe following Backstage Software Template implements a golden path for creating a new microservice:\n\n```yaml\napiVersion: scaffolder.backstage.io/v1beta3\nkind: Template\nmetadata:\n name: go-microservice\n title: Go Microservice (Golden Path)\n description: \"Production-ready Go service with CI/CD, observability, and Kubernetes manifests\"\n tags:\n - go\n - recommended\n - golden-path\nspec:\n owner: team-platform\n type: service\n parameters:\n - title: Service Details\n required:\n - name\n - owner\n - tier\n properties:\n name:\n title: Service Name\n type: string\n pattern: '^[a-z][a-z0-9-]{2,30}$'\n ui:help: 'Lowercase, hyphens only. This becomes the repo name, k8s namespace, and DNS entry.'\n owner:\n title: Owning Team\n type: string\n ui:field: OwnerPicker\n ui:options:\n catalogFilter:\n kind: Group\n tier:\n title: Service Tier\n type: string\n enum: ['critical', 'standard', 'experimental']\n enumNames: ['Critical (99.9% SLO)', 'Standard (99.5% SLO)', 'Experimental (best-effort)']\n - title: Infrastructure\n properties:\n needsDatabase:\n title: PostgreSQL Database\n type: boolean\n default: false\n needsRedis:\n title: Redis Cache\n type: boolean\n default: false\n needsQueue:\n title: SQS Queue\n type: boolean\n default: false\n steps:\n - id: fetch\n name: Fetch Template\n action: fetch:template\n input:\n url: ./skeleton\n values:\n name: ${{ parameters.name }}\n owner: ${{ parameters.owner }}\n tier: ${{ parameters.tier }}\n - id: publish\n name: Create Repository\n action: publish:github\n input:\n allowedHosts: ['github.com']\n repoUrl: 'github.com?owner=acme-corp&repo=${{ parameters.name }}'\n defaultBranch: main\n protectDefaultBranch: true\n requireCodeOwnerReviews: true\n - id: register\n name: Register in Catalog\n action: catalog:register\n input:\n repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}\n catalogInfoPath: '/catalog-info.yaml'\n```\n\nIn under two minutes, a developer goes from \"I need a new service\" to having a repository with CI/CD pipelines, Kubernetes manifests, a Dockerfile, structured logging, health checks, Prometheus metrics, and a Grafana dashboard, all pre-configured and following organizational standards."},{"heading":"Developer Experience Metrics Worth Tracking","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#developer-experience-metrics-worth-tracking","content":"Improvement requires measurement, and most platform teams measure the wrong things. \"Number of Backstage page views\" provides no actionable insight. The following metrics are worth tracking:\n\n```python\n# developer_experience_metrics.py\nfrom dataclasses import dataclass\nfrom datetime import timedelta\n\n@dataclass\nclass PlatformMetrics:\n # Time from \"I need X\" to \"X is provisioned and usable\"\n time_to_first_request: timedelta\n\n # Percentage of new services using golden path templates\n golden_path_adoption_rate: float\n\n # How many times developers had to ask for help on Slack\n # vs self-served through the portal\n self_service_ratio: float\n\n # DORA: time from commit to production\n lead_time_for_changes: timedelta\n\n # How often developers abandon a self-service flow midway\n action_abandonment_rate: float\n\n # Net Promoter Score from quarterly developer surveys\n developer_nps: int\n\n# Real numbers from a client engagement (anonymized)\nbefore_idp = PlatformMetrics(\n time_to_first_request=timedelta(days=5),\n golden_path_adoption_rate=0.0,\n self_service_ratio=0.12,\n lead_time_for_changes=timedelta(days=14),\n action_abandonment_rate=0.0, # no self-service existed\n developer_nps=-23\n)\n\nafter_idp_6_months = PlatformMetrics(\n time_to_first_request=timedelta(minutes=8),\n golden_path_adoption_rate=0.73,\n self_service_ratio=0.81,\n lead_time_for_changes=timedelta(hours=4),\n action_abandonment_rate=0.15,\n developer_nps=41\n)\n```\n\nThe before/after numbers from that engagement are striking. Five days to eight minutes for provisioning. But the metric that convinced leadership was the NPS swing, from -23 to +41. Developers went from actively hostile about internal tools to recommending the platform to new hires."},{"heading":"Where It Falls Apart","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#where-it-falls-apart","content":"Not everything is smooth. Here are the common pitfalls:\n\n**Backstage plugin quality is wildly inconsistent.** Some community plugins are excellent. Others haven't been updated in a year and break on every Backstage upgrade. Budget time for plugin maintenance or build your own.\n\n**Port's pricing scales with entities.** When you start modeling every Kubernetes pod, every Lambda function, every S3 bucket as an entity, the bill grows fast. Be deliberate about what you model.\n\n**The hardest part isn't technical.** It's convincing teams to register their services in the catalog and keep metadata up to date. A proven solution is a GitHub Action that fails CI if ` catalog-info.yaml ` is missing or has stale ownership info:\n\n```yaml\n# .github/workflows/catalog-lint.yml\nname: Catalog Info Lint\non: [pull_request]\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Validate catalog-info.yaml\n run: |\n if [ ! -f catalog-info.yaml ]; then\n echo \"::error::Missing catalog-info.yaml. See https://portal.acme.internal/docs/catalog-requirements\"\n exit 1\n fi\n # Validate owner exists in our org\n OWNER=$(yq '.spec.owner' catalog-info.yaml)\n if ! curl -sf \"https://portal.acme.internal/api/catalog/entities/by-name/group/default/${OWNER}\" > /dev/null; then\n echo \"::error::Owner '${OWNER}' not found in Backstage catalog. Update spec.owner.\"\n exit 1\n fi\n```"},{"heading":"Recommendation","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#recommendation","content":"For teams of under 100 developers wanting results in weeks, Port is the stronger starting point. The blueprint model gets you a working IDP fast, and the self-service actions are production-ready out of the box.\n\nFor organizations over 100 developers, or those needing deep customization or with strong opinions about data ownership, Backstage is the better fit. But commitment is essential, a half-built Backstage instance is worse than no developer portal at all.\n\nEither way, start with the service catalog. Get ownership right. Then add golden paths. Then self-service actions. Trying to do all three at once is how platform teams burn out and get dismantled.\n\nThis pattern has played out repeatedly across organizations. The teams that succeed treat the platform as a product with real users, real feedback loops, and real roadmaps. The teams that fail treat it as a project with a deadline and a handoff. There is no handoff. The platform is never done."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#questions","content":""},{"heading":"What's the difference between Backstage and Port?","level":3,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#question-1","content":"Backstage is an open-source framework you host and extend with plugins. Port is a SaaS platform with a declarative blueprint model. Backstage gives you full control but demands engineering investment; Port gets you running faster but with less customization depth."},{"heading":"How many engineers do I need to maintain an IDP built on Backstage?","level":3,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#question-2","content":"Realistically, plan for 1-2 dedicated platform engineers per 50-80 application developers. That ratio drops with Port since they handle the infrastructure, but you still need someone owning blueprints and integrations."},{"heading":"Are golden paths mandatory for all teams?","level":3,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#question-3","content":"No. Golden paths should be the path of least resistance, not a mandate. Teams that need to deviate should be able to, but the friction should be intentional and documented."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#article-tips","content":"1. **Tip:** Always start your IDP journey by measuring ` DORA metrics ` before building anything, you need a baseline or you're just guessing.\n2. **Tip:** Use Backstage's ` catalog-info.yaml ` as the single source of truth for service ownership. If it's not in the catalog, it doesn't exist.\n3. **Tip:** Golden paths should have escape hatches. If a developer *must* deviate, make the non-golden path visible but not blocked, ` softGate: true ` in your Port blueprint.\n\n- platform-engineering\n- backstage\n- port\n- idp\n- developer-experience\n- golden-paths\n- self-service\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/platform-engineering-idp-backstage-port"},{"text":"Backstage","url":"https://backstage.io/"},{"text":"Port","url":"https://www.getport.io/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port"},"headline":"Platform Engineering: Building Internal Developer Platforms with Backstage and Port","description":"A hands-on guide to building IDPs that developers actually want to use, covering Backstage plugins, Port blueprints, golden paths, and the developer experience metrics that matter.","datePublished":"2025-12-27","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"DevOps","keywords":"platform-engineering, backstage, port, idp, developer-experience, golden-paths, self-service","wordCount":1520,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/platform-engineering-idp-backstage-port"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Platform Engineering: Building Internal Developer Platforms with Backstage and Port","item":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What's the difference between Backstage and Port?","acceptedAnswer":{"@type":"Answer","text":"Backstage is an open-source framework you host and extend with plugins. Port is a SaaS platform with a declarative blueprint model. Backstage gives you full control but demands engineering investment; Port gets you running faster but with less customization depth."}},{"@type":"Question","name":"How many engineers do I need to maintain an IDP built on Backstage?","acceptedAnswer":{"@type":"Answer","text":"Realistically, plan for 1-2 dedicated platform engineers per 50-80 application developers. That ratio drops with Port since they handle the infrastructure, but you still need someone owning blueprints and integrations."}},{"@type":"Question","name":"Are golden paths mandatory for all teams?","acceptedAnswer":{"@type":"Answer","text":"No. Golden paths should be the path of least resistance, not a mandate. Teams that need to deviate should be able to, but the friction should be intentional and documented."}}]}]}],"contentHash":"8a93cced2b6cf2536e910de96aca6bf266a2c8ec77d4eb2a2540c3b4e187173f","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/platform-engineering-idp-backstage-port","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":67844,"contentBytes":14218}}
