Internal Developer Portals: Building Your Developer Hub

March 8, 2021

Internal developer portals have emerged as the central hub for engineering organizations. They aggregate documentation, service catalogs, and tooling into a single interface. Done well, they accelerate development. Done poorly, they become another abandoned wiki.

Here’s how to build developer portals that work.

What Developer Portals Provide

The Developer Experience Problem

Without a portal:

Find API docs       → Search wiki, Confluence, GitHub, Slack
Discover services   → Ask around, grep code, check Kubernetes
Create new service  → Copy from somewhere, miss half the setup
Understand ownership → Check git blame, hope it's current

With a portal:

Find API docs       → Portal → Service → API Documentation
Discover services   → Portal → Service Catalog → Browse/Search
Create new service  → Portal → Templates → Scaffold with best practices
Understand ownership → Portal → Service → Team, On-call, Runbooks

Core Capabilities

service_catalog:
  - Inventory of all services
  - Ownership information
  - Dependencies and relationships
  - Health status
  - Links to resources

documentation:
  - Technical docs (TechDocs)
  - API specifications
  - Runbooks
  - Architecture decisions

templates:
  - New service scaffolding
  - Infrastructure provisioning
  - Common patterns

search:
  - Unified search across all content
  - Code, docs, services, people

integrations:
  - CI/CD status
  - Monitoring dashboards
  - On-call schedules
  - Security scans

Backstage

What Is Backstage

Backstage is an open-source developer portal framework created by Spotify:

┌─────────────────────────────────────────────────────────────────┐
│                        Backstage                                 │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────┐  ┌────────────────┐  │
│  │ Service Catalog │  │    TechDocs     │  │   Templates    │  │
│  └─────────────────┘  └─────────────────┘  └────────────────┘  │
│                                                                  │
│  ┌─────────────────┐  ┌─────────────────┐  ┌────────────────┐  │
│  │    Search       │  │   Kubernetes    │  │    CI/CD       │  │
│  └─────────────────┘  └─────────────────┘  └────────────────┘  │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                      Plugin System                           ││
│  │   (100+ community plugins, custom plugins)                   ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

Getting Started

# Create Backstage app
npx @backstage/create-app

# Start development
cd my-backstage-app
yarn dev

Service Catalog

Define services in YAML:

# catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: orders-service
  description: Handles order processing
  annotations:
    github.com/project-slug: myorg/orders-service
    backstage.io/techdocs-ref: dir:.
  tags:
    - java
    - api
spec:
  type: service
  lifecycle: production
  owner: orders-team
  providesApis:
    - orders-api
  consumesApis:
    - payments-api
    - inventory-api
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: orders-api
  description: Orders REST API
spec:
  type: openapi
  lifecycle: production
  owner: orders-team
  definition:
    $text: ./openapi.yaml

TechDocs

Documentation as code:

# mkdocs.yml in service repo
site_name: Orders Service
nav:
  - Home: index.md
  - Architecture: architecture.md
  - API Reference: api.md
  - Runbooks:
      - Incident Response: runbooks/incidents.md
      - Deployment: runbooks/deployment.md

plugins:
  - techdocs-core
# docs/index.md
# Orders Service

The orders service handles order creation, management, and fulfillment.

## Quick Links
- [API Documentation](api.md)
- [Architecture](architecture.md)
- [Runbooks](runbooks/incidents.md)

Software Templates

Scaffold new services:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: java-service
  title: Java Microservice
  description: Create a new Java microservice with Spring Boot
spec:
  owner: platform-team
  type: service

  parameters:
    - title: Service Details
      required:
        - name
        - description
        - owner
      properties:
        name:
          title: Name
          type: string
        description:
          title: Description
          type: string
        owner:
          title: Owner
          type: string
          ui:field: OwnerPicker

    - title: Repository
      required:
        - repoUrl
      properties:
        repoUrl:
          title: Repository Location
          type: string
          ui:field: RepoUrlPicker

  steps:
    - id: fetch
      name: Fetch Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}

    - id: publish
      name: Publish to GitHub
      action: publish:github
      input:
        repoUrl: ${{ parameters.repoUrl }}

    - id: register
      name: Register Component
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: '/catalog-info.yaml'

  output:
    links:
      - title: Repository
        url: ${{ steps.publish.output.remoteUrl }}
      - title: Open in catalog
        icon: catalog
        entityRef: ${{ steps.register.output.entityRef }}

Building Your Portal

Start with Catalog

The service catalog is foundational:

rollout_phases:
  phase_1:
    - Import existing services
    - Basic ownership data
    - Links to repos and dashboards

  phase_2:
    - Dependency mapping
    - API documentation
    - Team information

  phase_3:
    - TechDocs integration
    - Templates for new services
    - CI/CD integration

Auto-Discovery

Don’t manually maintain everything:

discovery_sources:
  github:
    - Scan repos for catalog-info.yaml
    - Import automatically

  kubernetes:
    - Discover deployed services
    - Import metadata from labels

  terraform:
    - Import infrastructure components
    - Link to Terraform modules

Integrations

Connect to existing tools:

integrations:
  source_control:
    - GitHub/GitLab
    - Pull request status
    - Code owners

  ci_cd:
    - GitHub Actions
    - Jenkins
    - ArgoCD deployment status

  monitoring:
    - Prometheus/Grafana
    - PagerDuty on-call
    - Datadog dashboards

  security:
    - Vulnerability scans
    - Dependency audits
    - Security scores

Making It Successful

Adoption Strategies

adoption_tactics:
  make_it_useful:
    - Solve real pain points first
    - Integrate with daily workflows
    - Provide value before requiring effort

  make_it_easy:
    - Auto-populate what you can
    - Templates for common patterns
    - Search that actually works

  make_it_official:
    - Leadership support
    - Require for new services
    - Migrate docs from wiki

  iterate:
    - Start simple
    - Add features based on feedback
    - Measure adoption

Measuring Success

metrics:
  adoption:
    - Percentage of services cataloged
    - Active users per week
    - Template usage

  efficiency:
    - Time to create new service
    - Time to find documentation
    - Onboarding time reduction

  satisfaction:
    - Developer NPS
    - Feature requests
    - Support tickets

Common Pitfalls

avoid:
  empty_catalog:
    problem: Portal exists but nothing in it
    solution: Auto-discovery, seed with existing data

  stale_data:
    problem: Information becomes outdated
    solution: Generate from source, validate regularly

  too_complex:
    problem: Overwhelming number of features
    solution: Start simple, add based on need

  no_ownership:
    problem: Portal becomes orphaned
    solution: Dedicated team, clear roadmap

Customization

Custom Plugins

Extend Backstage with plugins:

// plugins/my-plugin/src/plugin.ts
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';

export const rootRouteRef = createRouteRef({
  id: 'my-plugin',
});

export const myPlugin = createPlugin({
  id: 'my-plugin',
  routes: {
    root: rootRouteRef,
  },
});

export const MyPluginPage = myPlugin.provide(
  createRoutableExtension({
    name: 'MyPluginPage',
    component: () => import('./components/MyPluginPage'),
    mountPoint: rootRouteRef,
  }),
);

Entity Cards

Add information to service pages:

// Display custom metrics on service page
export const ServiceMetricsCard = () => {
  const { entity } = useEntity();

  return (
    <InfoCard title="Metrics">
      <Typography>
        Requests/sec: {metrics.requestsPerSecond}
      </Typography>
      <Typography>
        Error rate: {metrics.errorRate}%
      </Typography>
    </InfoCard>
  );
};

Key Takeaways

A developer portal is only useful if developers use it. Focus on solving real problems and making it the easiest path to get things done.