Building Custom Backstage Plugins

Extending an internal developer portal requires a structured approach to plugin architecture, dependency management, and lifecycle governance. For tech leads, platform engineers, and engineering managers, mastering the Plugin Ecosystem & Custom Extensions provides the foundational framework needed to deliver scalable, maintainable tooling. This guide bridges high-level extension concepts with actionable implementation workflows, guiding teams from initial scaffolding through production deployment, debugging, and ongoing maintenance.

Prerequisites & Environment Setup

Before initiating development, ensure your engineering environment meets the baseline requirements for modern internal tooling. You will need Node.js 18+, the latest Backstage CLI (@backstage/cli), and strict TypeScript proficiency. Configure your workspace with rigid ESLint rules, Prettier formatting, and a dedicated monorepo structure to isolate plugin dependencies from the core application.

Establish clear ownership boundaries and define API contracts early to prevent cross-plugin coupling. Familiarity with the broader platform architecture ensures alignment with organizational governance standards and accelerates onboarding for new contributors.

Environment Initialization:

# Verify baseline tooling versions
node -v # >= 18.0.0
npm -v # >= 9.0.0
yarn -v # >= 1.22.0

# Install Backstage CLI globally or use npx
npx @backstage/cli --version

# Initialize monorepo workspace (if not already present)
yarn create @backstage/app
cd my-backstage-app

Configure strict TypeScript and linting in tsconfig.json and .eslintrc.js:

{
 "compilerOptions": {
 "strict": true,
 "noImplicitAny": true,
 "strictNullChecks": true,
 "esModuleInterop": true,
 "skipLibCheck": true
 }
}

Step-by-Step Configuration & Implementation

Begin by scaffolding your plugin using the official CLI. Define a unique plugin ID, select the appropriate type (frontend, backend, or common), and configure the initial routing table in packages/app/src/App.tsx.

# Scaffold a new frontend plugin
npx @backstage/create-plugin
# Follow prompts:
# - Plugin ID: custom-service-catalog
# - Plugin Type: frontend
# - Add GitHub integration: No

For UI-heavy implementations, follow the Step-by-step guide to creating a Backstage frontend plugin to establish component hierarchies, state management, and routing guards. Integrate backend services by exposing REST or GraphQL endpoints, then wire them to the frontend using @backstage/core-plugin-api and @backstage/frontend-plugin-api.

Update app-config.yaml to register plugin routes, configure environment-specific proxy rules, and define required environment variables for secure credential injection.

Proxy Configuration (app-config.yaml):

proxy:
 '/api/custom-plugin':
 target: '${CUSTOM_PLUGIN_API_TARGET}'
 changeOrigin: true
 secure: true
 headers:
 Authorization: 'Bearer ${CUSTOM_PLUGIN_API_TOKEN}'
 allowedMethods: ['GET', 'POST']
 allowedHeaders: ['Content-Type', 'Authorization']

Route Registration (packages/app/src/App.tsx):

import { createApp } from '@backstage/app-defaults';
import { customPlugin } from '@internal/backstage-plugin-custom';
import { catalogPlugin } from '@backstage/plugin-catalog';

const app = createApp({
 bindRoutes({ bind }) {
 bind(customPlugin.externalRoutes, {
 catalogIndex: catalogPlugin.routes.catalogIndex,
 });
 },
});

export default app;

Core Dependencies (plugins/custom-plugin/package.json):

{
 "name": "@internal/backstage-plugin-custom",
 "version": "1.0.0",
 "main": "src/index.ts",
 "types": "src/index.ts",
 "license": "Apache-2.0",
 "dependencies": {
 "@backstage/core-components": "^0.14.0",
 "@backstage/core-plugin-api": "^1.9.0",
 "@backstage/frontend-plugin-api": "^0.1.0",
 "react": "^18.2.0",
 "react-dom": "^18.2.0"
 },
 "devDependencies": {
 "@backstage/cli": "^0.26.0",
 "@types/react": "^18.2.0",
 "typescript": "^5.3.0"
 }
}

Validation & Testing Workflows

Rigorous validation prevents regressions in production portals and ensures consistent developer experiences. Implement Jest for unit testing React components and backend route handlers. Use Playwright or Cypress for end-to-end UI verification, simulating real user navigation and API interactions.

When your plugin interacts with entity data, validate against established Catalog Integration Patterns to ensure proper schema mapping, relationship resolution, and permission boundaries. Automate validation in CI by running yarn lint, yarn test, yarn tsc, and yarn build on every pull request. Deploy to a staging environment to verify routing, API connectivity, and role-based access controls before merging to the main branch.

CI/CD Pipeline Definition (.github/workflows/plugin-ci.yml):

name: Plugin CI/CD Pipeline
on:
 pull_request:
 branches: [main, develop]
 paths:
 - 'plugins/custom-plugin/**'
 - 'packages/app/**'
 - 'app-config.yaml'

jobs:
 validate-and-build:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: '18'
 cache: 'yarn'
 - run: yarn install --frozen-lockfile
 - run: yarn tsc --noEmit
 - run: yarn lint
 - run: yarn test --coverage
 - run: yarn build
 - name: Upload Build Artifact
 uses: actions/upload-artifact@v4
 with:
 name: plugin-dist
 path: plugins/custom-plugin/dist/

Maintenance & Lifecycle Management

Long-term plugin viability requires structured maintenance protocols and proactive dependency tracking. Pin major Backstage dependencies and schedule quarterly upgrades to align with upstream releases and security patches. Monitor plugin performance using OpenTelemetry traces, custom error boundary reporting, and structured logging. As your platform scales, standardize developer workflows by integrating plugin outputs with Scaffolder Template Design to automate repository creation, CI/CD pipeline provisioning, and environment bootstrapping. Document deprecation paths, maintain backward compatibility layers, and establish clear communication channels to prevent breaking changes for consuming engineering teams.

Production Deployment Strategy

Deploy plugins as part of the monolithic Backstage application or as isolated micro-frontends using Webpack Module Federation. For Kubernetes deployments, package the application using Helm:

# Build production bundle
yarn build:backend --config app-config.production.yaml

# Containerize
docker build -t registry.internal/backstage-app:$(git rev-parse --short HEAD) .

# Deploy via Helm
helm upgrade --install backstage ./charts/backstage \
 --set image.tag=$(git rev-parse --short HEAD) \
 --set env.CUSTOM_PLUGIN_API_TARGET=https://api.internal.corp/v1

Debugging & Observability

Enable verbose logging during development:

LOG_LEVEL=debug yarn dev

In production, instrument the plugin with OpenTelemetry:

import { createSpan } from '@backstage/plugin-opentelemetry';
export const fetchData = async () => {
 const span = createSpan('custom-plugin.fetchData');
 try {
 const res = await fetch('/api/custom-plugin/data');
 span.setStatus({ code: 200 });
 return res.json();
 } catch (err) {
 span.recordException(err);
 throw err;
 } finally {
 span.end();
 }
};

Use browser DevTools network throttling and React Profiler to identify render bottlenecks. Correlate frontend errors with backend traces via trace-id headers injected by the proxy.

Rollback Procedures

Maintain strict version control over plugin releases. If a deployment introduces regressions:

  1. Traffic Shift: Update ingress routing to point to the previous stable Docker image tag.
  2. Database/Config Revert: If schema migrations were applied, execute the inverse migration script. Revert app-config.yaml to the previous Git commit.
  3. Frontend Cache Purge: Invalidate CDN and service worker caches to prevent stale asset loading.
  4. Post-Mortem: Analyze error boundary logs, trace failures, and update integration tests to prevent recurrence.

Common Pitfalls

  • Hardcoding API endpoints or credentials instead of leveraging environment variables and proxy configurations.
  • Ignoring Backstage’s permission framework, leading to unauthorized data exposure or broken RBAC enforcement.
  • Over-coupling frontend components with backend logic, which prevents independent deployment and scaling.
  • Skipping strict type validation for catalog entity payloads, causing runtime rendering failures.
  • Neglecting CI/CD pipeline optimization, resulting in slow build times and delayed developer feedback loops.

Frequently Asked Questions

How do I handle authentication between custom plugins and external APIs? Use Backstage’s built-in proxy configuration to inject credentials securely via environment variables. For user-specific authentication, implement OAuth2/OIDC flows through the @backstage/core-plugin-api identity provider and attach tokens to outbound requests.

Can I share state between multiple custom plugins? Yes, but it should be done cautiously. Use the @backstage/frontend-plugin-api to expose shared APIs or leverage a centralized state management library. Avoid direct DOM manipulation or cross-plugin imports that create tight coupling.

What is the recommended CI/CD pipeline for Backstage plugin deployments? Implement a multi-stage pipeline: lint and type-check on PR, run unit/integration tests in isolation, build the plugin package, deploy to a staging environment, and execute smoke tests before promoting to production. Use monorepo-aware tools like Turborepo or Nx to optimize build caching.

How do I migrate legacy internal tools into Backstage plugins? Start by wrapping the legacy tool’s API in a Backstage backend plugin, then build a lightweight frontend UI using React and Backstage components. Gradually replace legacy UI routes with the new plugin, maintaining feature parity and establishing deprecation notices for the old system.