Internal Developer Platform IDP: Building Enterprise Developer Portal with Backstage 2026

DevOps运维

Internal Developer Platform IDP: Building Enterprise Developer Portal with Backstage

You just joined a company with 200 microservices. Day one, your lead says "deploy order-service." Then you spend 3 hours finding the Git repo, 2 hours finding the CI/CD config, 1 hour finding the API docs — the day is gone before you've written a single line of code.

This is daily life without an Internal Developer Platform (IDP). Backstage, the developer portal framework open-sourced by Spotify, is becoming the de facto standard for platform engineering. This article walks through 5 core patterns to build an enterprise-grade IDP from scratch.

Core Concepts at a Glance

Concept Description Analogy
IDP (Internal Developer Platform) Unified self-service portal for developers Enterprise "App Store"
Backstage Open-source IDP framework by Spotify The Kubernetes of IDP
Software Catalog Metadata registry for all services "Yellow Pages" for services
TechDocs Markdown-based technical documentation system Engineering-grade Wiki
Software Template One-click standardized project creation Web-based scaffolding CLI
Plugin Backstage extension module Browser extension

5 Major Pain Points Without an IDP

  1. Service discovery difficulty: Hundreds of microservices, can't find who owns what, how to deploy, where to monitor
  2. Scattered documentation: Confluence, GitLab Wiki, Notion... can't find it, can't understand it
  3. Slow project onboarding: Rebuilding scaffolding from scratch every time, CI/CD, monitoring, logging config is repetitive
  4. Fragmented toolchain: Jenkins, ArgoCD, Grafana, Jira... constant context switching kills productivity
  5. Compliance audit nightmare: Who changed what, who has access, service dependency graph is a mess

Pattern 1: Backstage Architecture and Installation

Backstage uses a plugin-based monolith architecture. The core consists of four modules: Catalog + TechDocs + Scaffold + Search, extended through plugins.

# Backstage installation and initialization
# Environment: Node.js 20.x+ / yarn 1.22+ / Git 2.40+
# Dependencies: @backstage/cli 1.32+ / @backstage/core 1.30+

# 1. Create Backstage app
npx @backstage/create-app@latest --name my-idp
cd my-idp

# 2. Project structure
# my-idp/
# ├── app/                    # Frontend React app
# │   ├── src/
# │   │   ├── components/     # Custom components
# │   │   ├── plugins/        # Frontend plugins
# │   │   └── App.tsx         # App entry point
# ├── packages/
# │   ├── backend/            # Backend Node.js service
# │   └── app/                # Frontend build package
# ├── plugins/                # Custom plugin directory
# ├── catalog-info.yaml       # Catalog entity definitions
# └── app-config.yaml         # App configuration

# 3. Start dev server
yarn dev

# 4. Build production version
yarn build:backend
# app-config.yaml: Backstage core configuration
# Environment: Backstage 1.32+
app:
  title: Enterprise Developer Portal
  baseUrl: http://localhost:3000

organization:
  name: MyCompany

backend:
  baseUrl: http://localhost:7007
  listen:
    port: 7007
  csp:
    connect-src: ["'self'", 'http:', 'https:']
  cors:
    origin: http://localhost:3000
    methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
    credentials: true
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT:-5432}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      database: backstage_plugin

integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}
  gitlab:
    - host: gitlab.mycompany.com
      token: ${GITLAB_TOKEN}

auth:
  environment: development
  providers:
    github:
      development:
        clientId: ${GITHUB_CLIENT_ID}
        clientSecret: ${GITHUB_CLIENT_SECRET}
    oidc:
      production:
        metadataUrl: ${OIDC_METADATA_URL}
        clientId: ${OIDC_CLIENT_ID}
        clientSecret: ${OIDC_CLIENT_SECRET}

catalog:
  rules:
    - allow: [Component, System, API, Resource, Location, Template]
  locations:
    - type: url
      target: https://github.com/mycompany/catalog/blob/main/catalog-info.yaml
    - type: url
      target: https://gitlab.mycompany.com/platform/catalog/-/blob/main/all.yaml

techdocs:
  builder: 'local'
  generator:
    runIn: 'docker'
  publisher:
    type: 'local'

Pattern 2: Software Catalog

The Catalog is the heart of Backstage — the metadata registry for all services. Define entities through YAML descriptor files, with automatic service discovery and indexing.

# catalog-info.yaml: Service component registration
# Environment: Backstage 1.32+ / Catalog API v1
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: order-service
  description: Core order service handling order creation, payment, and fulfillment
  annotations:
    backstage.io/source-location: url:https://github.com/mycompany/order-service
    backstage.io/techdocs-ref: dir:.
    github.com/project-slug: mycompany/order-service
    gitlab.com/project-id: "12345"
    jira.com/project-key: ORD
    grafana/dashboard-tag: order-service
    prometheus.io/alert: order-service-alerts
  tags:
    - java
    - spring-boot
    - microservice
    - tier-1
  links:
    - url: https://grafana.mycompany.com/d/order-service
      title: Grafana Monitoring
      icon: dashboard
    - url: https://argocd.mycompany.com/applications/order-service
      title: ArgoCD Deployment
      icon: cloud
    - url: https://order-service.mycompany.com/swagger
      title: API Docs
      icon: api
spec:
  type: service
  lifecycle: production
  owner: team-order
  system: order-system
  dependsOn:
    - component:payment-service
    - component:inventory-service
    - resource:order-db
  providesApis:
    - order-api
  consumesApis:
    - payment-api
    - inventory-api
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: order-api
  description: Order Service REST API
spec:
  type: openapi
  lifecycle: production
  owner: team-order
  system: order-system
  definition:
    $text: https://raw.githubusercontent.com/mycompany/order-service/main/openapi.yaml
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
  name: order-system
  description: Order system including order, payment, and inventory services
spec:
  owner: team-order
  domain: commerce
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
  name: team-order
  description: Order Team
spec:
  type: team
  parent: dept-commerce
  children: []
  members:
    - john.doe
    - jane.smith
    - bob.wilson
// TypeScript: Custom Catalog entity processor
// Environment: Node.js 20.x+ / @backstage/plugin-catalog-node 1.12+
import {
  CatalogProcessor,
  CatalogProcessorEmit,
  processingResult,
} from '@backstage/plugin-catalog-node';
import { LocationSpec, Location } from '@backstage/plugin-catalog-common';
import { Entity } from '@backstage/catalog-model';

export class CustomServiceProcessor implements CatalogProcessor {
  getProcessorName(): string {
    return 'CustomServiceProcessor';
  }

  async validateLocationKind(location: Location): Promise<boolean> {
    return location.type === 'url';
  }

  async preProcessEntity(
    entity: Entity,
    location: LocationSpec,
    emit: CatalogProcessorEmit,
  ): Promise<Entity> {
    // Auto-add compliance tags for all production services
    if (
      entity.kind === 'Component' &&
      entity.spec?.lifecycle === 'production'
    ) {
      const annotations = entity.metadata.annotations ?? {};
      annotations['compliance.mycompany.com/scan-required'] = 'true';
      annotations['compliance.mycompany.com/last-scan'] = new Date().toISOString();
      entity.metadata.annotations = annotations;
    }

    // Auto-generate service dependency relations
    if (entity.kind === 'Component' && entity.spec?.dependsOn) {
      const deps = entity.spec.dependsOn as string[];
      for (const dep of deps) {
        emit(
          processingResult.relation({
            source: {
              name: entity.metadata.name,
              namespace: entity.metadata.namespace ?? 'default',
              kind: entity.kind,
            },
            target: {
              name: dep.replace('component:', ''),
              namespace: 'default',
              kind: 'Component',
            },
            type: 'dependsOn',
          }),
        );
      }
    }

    return entity;
  }
}

Pattern 3: TechDocs

TechDocs is built on MkDocs + Markdown, following the Docs-as-Code philosophy — documentation lives alongside code in the same repository lifecycle.

# mkdocs.yaml: TechDocs configuration
# Environment: MkDocs 1.6+ / mkdocs-material 9.5+
site_name: Order Service Technical Docs
site_description: Complete technical documentation for Order Service

nav:
  - Home: index.md
  - Getting Started: getting-started.md
  - Architecture:
    - System Overview: architecture/overview.md
    - Data Model: architecture/data-model.md
    - API Design: architecture/api-design.md
  - Operations:
    - Deployment Guide: ops/deployment.md
    - Monitoring & Alerts: ops/monitoring.md
    - Troubleshooting: ops/troubleshooting.md
  - Development:
    - Coding Standards: dev/coding-standards.md
    - Git Workflow: dev/git-workflow.md
    - Testing Strategy: dev/testing.md

plugins:
  - techdocs-core:
      add_mermaid: true
      add_plantuml: true

markdown_extensions:
  - admonition
  - codehilite
  - footnotes
  - meta
  - toc:
      permalink: true
  - pymdownx.superfences:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format
<!-- docs/index.md: TechDocs homepage -->
# Order Service

## Overview

Order Service is the core microservice of the order system, handling order creation, payment, and fulfillment.

## Quick Links

| Resource | Link |
|----------|------|
| Source Code | [GitHub](https://github.com/mycompany/order-service) |
| CI/CD | [Jenkins](https://jenkins.mycompany.com/job/order-service) |
| Monitoring | [Grafana](https://grafana.mycompany.com/d/order-service) |
| API Docs | [Swagger](https://order-service.mycompany.com/swagger) |

## Architecture

```mermaid
graph TB
    A[API Gateway] --> B[Order Service]
    B --> C[Payment Service]
    B --> D[Inventory Service]
    B --> E[(Order DB)]
    B --> F[Redis Cache]
    B --> G[Kafka]

Key Metrics

Metric Target Current
P99 Latency <200ms 150ms
Availability 99.95% 99.97%
Error Rate <0.1% 0.05%

## Pattern 4: Plugin Development

Backstage's plugin system is its core competitive advantage. Custom plugins let the IDP truly adapt to your enterprise toolchain.

```typescript
// TypeScript: Backstage custom plugin - Service Health Check Panel
// Environment: Node.js 20.x+ / @backstage/core-plugin-api 1.9+ / React 18.x

// --- Frontend Plugin ---

// plugins/service-health/src/plugin.ts
import {
  createPlugin,
  createRoutableExtension,
} from '@backstage/core-plugin-api';

export const serviceHealthPlugin = createPlugin({
  id: 'service-health',
  routes: {
    root: rootRouteRef,
  },
});

export const ServiceHealthPage = serviceHealthPlugin.provide(
  createRoutableExtension({
    name: 'ServiceHealthPage',
    component: () =>
      import('./components/ServiceHealthPanel').then(
        (m) => m.ServiceHealthPanel,
      ),
    mountPoint: rootRouteRef,
  }),
);

// plugins/service-health/src/components/ServiceHealthPanel.tsx
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
  Header,
  Page,
  Content,
  InfoCard,
  Progress,
  StatusOK,
  StatusError,
  StatusWarning,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { serviceHealthApiRef } from '../api';

interface HealthCheckResult {
  name: string;
  status: 'healthy' | 'degraded' | 'down';
  latency_ms: number;
  last_check: string;
  message: string;
}

export const ServiceHealthPanel: React.FC = () => {
  const { entity } = useEntity();
  const healthApi = useApi(serviceHealthApiRef);
  const [healthData, setHealthData] = React.useState<HealthCheckResult[]>([]);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    const serviceName = entity.metadata.name;
    healthApi
      .getServiceHealth(serviceName)
      .then((data) => {
        setHealthData(data.checks);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, [entity, healthApi]);

  if (loading) return <Progress />;

  const StatusIcon = ({ status }: { status: string }) => {
    switch (status) {
      case 'healthy':
        return <StatusOK />;
      case 'degraded':
        return <StatusWarning />;
      case 'down':
        return <StatusError />;
      default:
        return null;
    }
  };

  return (
    <Page themeId="tool">
      <Header title={`${entity.metadata.name} Health Check`} />
      <Content>
        {healthData.map((check) => (
          <InfoCard key={check.name} title={check.name}>
            <StatusIcon status={check.status} />
            <p>Status: {check.status}</p>
            <p>Latency: {check.latency_ms}ms</p>
            <p>Last Check: {check.last_check}</p>
            <p>Message: {check.message}</p>
          </InfoCard>
        ))}
      </Content>
    </Page>
  );
};
// TypeScript: Backstage backend plugin
// Environment: Node.js 20.x+ / @backstage/backend-common 0.25+

// plugins/service-health-backend/src/plugin.ts
import {
  createBackendPlugin,
  coreServices,
} from '@backstage/backend-plugin-api';
import { createRouter } from './router';

export const serviceHealthBackendPlugin = createBackendPlugin({
  pluginId: 'service-health',
  register(env) {
    env.registerInit({
      deps: {
        logger: coreServices.logger,
        config: coreServices.rootConfig,
        httpRouter: coreServices.httpRouter,
        discovery: coreServices.discovery,
      },
      async init({ logger, config, httpRouter, discovery }) {
        const router = await createRouter({
          logger,
          config,
          discovery,
        });

        httpRouter.use(router);
        httpRouter.addAuthPolicy({
          path: '/health',
          allow: 'unauthenticated',
        });

        logger.info('Service Health plugin initialized');
      },
    });
  },
});

// plugins/service-health-backend/src/router.ts
import express, { Router } from 'express';
import { LoggerService, RootConfigService, DiscoveryService } from '@backstage/backend-plugin-api';

interface RouterOptions {
  logger: LoggerService;
  config: RootConfigService;
  discovery: DiscoveryService;
}

export async function createRouter(
  options: RouterOptions,
): Promise<Router> {
  const { logger, config } = options;
  const router = Router();

  router.get('/health', (_req, res) => {
    res.json({ status: 'ok' });
  });

  router.get('/services/:serviceName/health', async (req, res) => {
    const { serviceName } = req.params;

    try {
      const healthChecks = await fetchServiceHealth(config, serviceName);
      res.json({
        service: serviceName,
        checks: healthChecks,
        timestamp: new Date().toISOString(),
      });
    } catch (error) {
      logger.error(`Failed to fetch health for ${serviceName}: ${error}`);
      res.status(500).json({ error: 'Health check failed' });
    }
  });

  return router;
}

async function fetchServiceHealth(
  config: RootConfigService,
  serviceName: string,
): Promise<Array<{
  name: string;
  status: 'healthy' | 'degraded' | 'down';
  latency_ms: number;
  last_check: string;
  message: string;
}>> {
  const prometheusUrl = config.getString('monitoring.prometheus.url');

  return [
    {
      name: 'HTTP Endpoint',
      status: 'healthy',
      latency_ms: 45,
      last_check: new Date().toISOString(),
      message: 'All endpoints responding normally',
    },
    {
      name: 'Database Connection',
      status: 'healthy',
      latency_ms: 12,
      last_check: new Date().toISOString(),
      message: 'Connection pool healthy',
    },
    {
      name: 'Kafka Consumer',
      status: 'degraded',
      latency_ms: 250,
      last_check: new Date().toISOString(),
      message: 'Consumer lag exceeds threshold',
    },
  ];
}

Pattern 5: Production-Grade IDP Deployment

# docker-compose.yml: Backstage production deployment
# Environment: Docker Compose v2.30+ / PostgreSQL 16
version: "3.8"

services:
  backstage:
    image: mycompany/backstage:1.32.0
    ports:
      - "7007:7007"
    environment:
      APP_CONFIG_app_baseUrl: https://idp.mycompany.com
      APP_CONFIG_backend_baseUrl: https://idp.mycompany.com
      APP_CONFIG_backend_database_client: pg
      APP_CONFIG_backend_database_connection_host: postgres
      APP_CONFIG_backend_database_connection_port: "5432"
      APP_CONFIG_backend_database_connection_user: ${POSTGRES_USER}
      APP_CONFIG_backend_database_connection_password: ${POSTGRES_PASSWORD}
      POSTGRES_HOST: postgres
      POSTGRES_PORT: "5432"
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      GITHUB_TOKEN: ${GITHUB_TOKEN}
      GITLAB_TOKEN: ${GITLAB_TOKEN}
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:7007/healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: backstage_plugin
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
# kubernetes/backstage-deployment.yaml: K8s production deployment
# Environment: Kubernetes 1.30+ / Helm 3.15+
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backstage
  namespace: idp
  labels:
    app: backstage
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backstage
  template:
    metadata:
      labels:
        app: backstage
    spec:
      containers:
        - name: backstage
          image: mycompany/backstage:1.32.0
          ports:
            - containerPort: 7007
          env:
            - name: APP_CONFIG_app_baseUrl
              value: "https://idp.mycompany.com"
            - name: APP_CONFIG_backend_baseUrl
              value: "https://idp.mycompany.com"
            - name: APP_CONFIG_backend_database_client
              value: "pg"
            - name: APP_CONFIG_backend_database_connection_host
              valueFrom:
                secretKeyRef:
                  name: backstage-db-secret
                  key: host
            - name: APP_CONFIG_backend_database_connection_password
              valueFrom:
                secretKeyRef:
                  name: backstage-db-secret
                  key: password
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2000m"
              memory: "2Gi"
          livenessProbe:
            httpGet:
              path: /healthcheck
              port: 7007
            initialDelaySeconds: 30
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /healthcheck
              port: 7007
            initialDelaySeconds: 10
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: backstage
  namespace: idp
spec:
  selector:
    app: backstage
  ports:
    - port: 80
      targetPort: 7007
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: backstage
  namespace: idp
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - hosts:
        - idp.mycompany.com
      secretName: backstage-tls
  rules:
    - host: idp.mycompany.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: backstage
                port:
                  number: 80
# Helm deployment (recommended)
# Environment: Helm 3.15+ / Kubernetes 1.30+

# 1. Add Backstage Helm repo
helm repo add backstage https://backstage.github.io/charts
helm repo update

# 2. Create values.yaml override
cat > backstage-values.yaml << 'EOF'
backstage:
  image:
    repository: mycompany/backstage
    tag: 1.32.0
  extraAppConfig:
    - configMapRef: backstage-config
  extraEnvVarsSecrets:
    - backstage-secrets

ingress:
  enabled: true
  host: idp.mycompany.com
  tls:
    enabled: true
    secretName: backstage-tls

postgresql:
  enabled: true
  primary:
    persistence:
      size: 20Gi
  auth:
    existingSecret: backstage-db-secret
EOF

# 3. Deploy
helm install backstage backstage/backstage \
  -n idp \
  --create-namespace \
  -f backstage-values.yaml

# 4. Verify
kubectl get pods -n idp
kubectl port-forward svc/backstage 7007:80 -n idp

Pitfall Guide: 5 Common Traps

Trap 1: Catalog Entity Not Showing After Registration

❌ Wrong: Only creating catalog-info.yaml locally without registering a Location
✅ Right: Add locations in app-config.yaml, or register Location in the UI

Trap 2: TechDocs Build Failures

❌ Wrong: Running MkDocs directly on the host, missing dependencies cause build failures
✅ Right: Configure techdocs.generator.runIn: 'docker' for containerized builds

Trap 3: Insufficient GitHub/GitLab Integration Token Permissions

# ❌ Wrong: Token only has repo:read permission
# ✅ Right: Token needs these permissions
# GitHub: repo, read:org, read:user
# GitLab: api, read_repository, read_api

Trap 4: Plugin Hot Reload Not Working

# ❌ Wrong: Not restarting dev server after modifying plugin code
# ✅ Right: Ensure yarn dev is running, Backstage supports HMR hot reload
# If HMR doesn't work, clear cache and restart:
yarn cache clean
yarn dev

Trap 5: Production Database Connection Pool Exhaustion

# app-config.yaml: Configure connection pool
backend:
  database:
    client: pg
    connection:
      host: postgres
      port: 5432
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      pool:
        min: 5
        max: 20
        idleTimeoutMillis: 30000
        connectionTimeoutMillis: 5000

Error Troubleshooting Table

Error Message Cause Solution
Failed to fetch catalog entities Invalid GitHub/GitLab Token or insufficient permissions Check Token permissions, ensure repo and read:org
ECONNREFUSED 127.0.0.1:5432 PostgreSQL not running or wrong connection config Check database service and connection params
TechDocs generation failed MkDocs build failure, missing dependencies Set runIn: 'docker', or install missing Python packages
Plugin not found: xxx Plugin not registered in backend Import and register plugin in packages/backend/src/index.ts
Unauthorized: Invalid token OIDC/OAuth misconfiguration Check clientId/clientSecret and callback URL
CORS error Frontend/backend domain mismatch Configure backend.cors.origin to match frontend URL
Location not found catalog-info.yaml URL unreachable Verify URL accessibility and Token permissions
Out of memory Node.js heap memory insufficient Set NODE_OPTIONS=--max-old-space-size=4096
Schema validation failed catalog-info.yaml format error Validate with backstage-cli catalog-validate
Template execution timeout Software Template execution timeout Check HTTP requests in template, increase timeout config

Advanced Optimization: 5 Production-Grade Tips

Tip 1: Catalog Auto-Discovery

// TypeScript: GitLab auto-discovery processor
// Environment: @backstage/plugin-catalog-backend-module-gitlab 0.8+
import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab';

const config = {
  catalog: {
    processors: [
      GitLabDiscoveryProcessor.fromConfig(env.config, {
        logger: env.logger,
        schedule: {
          frequency: { minutes: 30 },
          timeout: { minutes: 3 },
          initialDelay: { seconds: 15 },
        },
      }),
    ],
  },
};

Tip 2: RBAC Permission Control

# app-config.yaml: RBAC configuration
# Environment: @backstage/plugin-permission-backend 0.6+
permission:
  enabled: true
  rbac:
    policies:
      - name: 'role:default/developer'
        permissions:
          - 'catalog.entity.read'
          - 'techdocs.read'
          - 'scaffolder.template.execute'
      - name: 'role:default/admin'
        permissions:
          - 'catalog.entity.create'
          - 'catalog.entity.delete'
          - 'catalog.entity.update'
          - 'techdocs.write'
          - 'scaffolder.template.create'
    policyFile: ./rbac-policy.csv

Tip 3: Search Enhancement

# app-config.yaml: Search engine configuration
# Environment: @backstage/plugin-search-backend-module-elasticsearch 1.6+
search:
  collators:
    techdocs:
      schedule:
        frequency: { minutes: 10 }
        timeout: { minutes: 5 }
        initialDelay: { seconds: 30 }
  elasticsearch:
    url: ${ELASTICSEARCH_URL}
    auth:
      username: ${ELASTICSEARCH_USER}
      password: ${ELASTICSEARCH_PASSWORD}
    indexPrefix: backstage_search_

Tip 4: Multi-Cluster ArgoCD Integration

# app-config.yaml: ArgoCD multi-cluster integration
# Environment: @backstage/plugin-argocd 2.5+
argocd:
  appLocatorMethods:
    - type: 'config'
      instances:
        - name: production
          url: https://argocd.mycompany.com
          token: ${ARGOCD_TOKEN_PROD}
        - name: staging
          url: https://argocd-staging.mycompany.com
          token: ${ARGOCD_TOKEN_STAGING}

Tip 5: Golden Path Templates

# templates/java-spring-boot/template.yaml: Golden Path scaffolding template
# Environment: @backstage/plugin-scaffolder 1.25+
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: java-spring-boot-service
  title: Java Spring Boot Microservice Template
  description: Create a standardized Spring Boot microservice with CI/CD, monitoring, and docs
spec:
  owner: platform-team
  type: service
  parameters:
    - title: Service Information
      required:
        - name
        - owner
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z][a-z0-9-]*$'
          description: Lowercase letters, numbers, and hyphens only
        owner:
          title: Owner Team
          type: string
          ui:field: OwnerPicker
          ui:options:
            allowedKinds:
              - Group
        description:
          title: Service Description
          type: string
          description: One-line description of service functionality
        database:
          title: Database Type
          type: string
          default: postgresql
          enum:
            - postgresql
            - mysql
            - mongodb
            - none
    - title: CI/CD Configuration
      properties:
        ciProvider:
          title: CI Tool
          type: string
          default: github-actions
          enum:
            - github-actions
            - gitlab-ci
            - jenkins
        deployTarget:
          title: Deploy Target
          type: string
          default: kubernetes
          enum:
            - kubernetes
            - ecs
            - cloud-run

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

    - id: publish
      name: Publish to Git Repository
      action: publish:github
      input:
        allowedHosts: ['github.com']
        description: ${{ parameters.description }}
        repoUrl: github.com?owner=mycompany&repo=${{ parameters.name }}
        defaultBranch: main
        protectDefaultBranch: true

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

  output:
    links:
      - title: Repository URL
        url: ${{ steps.publish.output.remoteUrl }}
      - title: Open in Catalog
        url: https://idp.mycompany.com/catalog/default/component/${{ parameters.name }}

IDP Platform Comparison

Dimension Backstage Port Humanitec
Open Source ✅ Apache 2.0 ❌ Commercial SaaS ❌ Commercial SaaS
Self-Hosted ✅ Full control ❌ Cloud-hosted ❌ Cloud-hosted
Plugin Ecosystem 200+ community plugins Built-in integrations Built-in integrations
Learning Curve Medium-High (TypeScript) Low (visual config) Medium (platform engineering)
Customization Very High (code-level) Medium (API extension) Medium (Score definition)
Software Catalog ✅ Core feature ✅ Core feature ✅ Core feature
TechDocs ✅ Native support ❌ Needs integration ❌ Needs integration
Cost Engineering effort $50-200/user/month $100-500/user/month
Scale Fit 50-5000+ developers 10-500 developers 100-5000+ developers
Notable Customers Spotify, American Airlines Lyft, Palo Alto BMW, Qonto

Conclusion

Backstage is the optimal choice for building an Internal Developer Platform in 2026 — open source, self-hostable, with a rich plugin ecosystem. But note:

  • Small teams (<50): Consider Port or similar SaaS solutions first; Backstage maintenance may not be cost-effective
  • Medium teams (50-500): Backstage is the best choice, 2-3 engineers for maintenance
  • Large teams (500+): Backstage + Golden Path templates + auto-discovery for a complete platform engineering system

Key success factors: Start with Catalog and TechDocs, gradually add plugins, then build Golden Path templates. Don't try to boil the ocean.

Try these browser-local tools — no sign-up required →

#内部开发者平台#Backstage#IDP#开发者门户#平台工程#2026#DevOps运维