Skip to main content

Plugin Development

Version: Backstage v1.42.0+ Status: Complete Reference Last Updated: 2025-10-27

Overview​

This guide covers creating frontend plugins for the New Frontend System. Plugins are packages that provide extensions (pages, APIs, themes, etc.) to Backstage apps.

Table of Contents​

  1. What is a Frontend Plugin?
  2. Plugin Structure
  3. Creating a Plugin
  4. Alpha Exports
  5. Providing Extensions
  6. Plugin Configuration
  7. Frontend Modules
  8. Publishing Plugins
  9. Best Practices

What is a Frontend Plugin?​

A frontend plugin is a collection of extensions packaged as an npm module. Plugins:

  • Provide one or more extensions (pages, APIs, nav items, etc.)
  • Can be installed via package.json
  • Are automatically discovered by the app
  • Can be configured via app-config.yaml
  • Are independently versioned and published

Plugin vs Extension vs Module​

  • Extension: Single unit of functionality (page, API, theme)
  • Plugin: Collection of extensions from a single package
  • Frontend Module: Extension to an existing plugin without modifying it

Plugin Structure​

Directory Layout​

packages/
└── my-plugin/
├── package.json
├── src/
│ ├── index.ts # Public API (legacy components)
│ ├── alpha.ts # New frontend system exports
│ ├── plugin.ts # Plugin definition
│ ├── routes.ts # Route refs
│ ├── components/ # React components
│ │ ├── MyPage/
│ │ └── MyWidget/
│ └── api/ # API definitions
│ ├── types.ts
│ └── MyApiClient.ts
├── dev/
│ └── index.tsx # Dev environment
└── README.md

package.json​

{
"name": "@backstage-community/plugin-my-plugin",
"version": "1.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./package.json": "./package.json"
},
"backstage": {
"role": "frontend-plugin"
},
"dependencies": {
"@backstage/core-components": "^0.15.0",
"@backstage/core-plugin-api": "^1.10.0",
"@backstage/frontend-plugin-api": "^0.9.0",
"react": "^18.0.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}

Key Points:

  • exports field includes /alpha subpath
  • backstage.role identifies plugin type
  • Both legacy and new frontend system dependencies

Creating a Plugin​

Using Backstage CLI​

# From workspace root
yarn backstage-cli create --select plugin

# Or with new frontend plugin template
yarn backstage-cli create --select plugin --option id=my-plugin

Manual Plugin Creation​

1. Create Plugin Definition​

// src/plugin.ts
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
import { myPageExtension } from './extensions';

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
myPageExtension,
// More extensions...
],
});

2. Create Extensions​

// src/extensions.ts
import { PageBlueprint } from '@backstage/frontend-plugin-api';

export const myPageExtension = PageBlueprint.make({
name: 'my-page',
params: {
defaultPath: '/my-plugin',
loader: async () => {
const { MyPage } = await import('./components/MyPage');
return <MyPage />;
},
},
});

3. Create Alpha Exports​

// src/alpha.ts
export { myPlugin as default } from './plugin';
export { myPageExtension } from './extensions';

4. Create Page Component​

// src/components/MyPage/MyPage.tsx
import React from 'react';
import { Header, Page, Content } from '@backstage/core-components';

export const MyPage = () => {
return (
<Page themeId="tool">
<Header title="My Plugin" subtitle="Welcome to my plugin" />
<Content>
<div>Hello from My Plugin!</div>
</Content>
</Page>
);
};

Alpha Exports​

The /alpha subpath is where new frontend system exports live.

Why Alpha Exports?​

  • Separation: Keep new system exports separate from legacy
  • Compatibility: Support both systems during migration
  • Discovery: Apps can auto-discover alpha exports
  • Versioning: Signal that APIs may change

Alpha Export Structure​

// src/alpha.ts

/**
* @alpha
* Frontend plugin for My Plugin
*/
export { myPlugin as default } from './plugin';

/**
* @alpha
* Extensions provided by this plugin
*/
export { myPageExtension, myWidgetExtension } from './extensions';

/**
* @alpha
* API definitions
*/
export { myApiRef, type MyApi } from './api';

/**
* @alpha
* Utility components (if needed by app customizations)
*/
export { MyCustomComponent } from './components';

Importing Alpha Exports​

// In app
import myPlugin from '@backstage-community/plugin-my-plugin/alpha';

createApp({
features: [myPlugin],
});

Auto-Discovery of Alpha Exports​

Enable in app-config.yaml:

app:
packages:
- all # Auto-discover all plugins with /alpha exports

With auto-discovery, no manual imports needed!


Providing Extensions​

Plugins can provide multiple types of extensions.

Multiple Page Extensions​

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
// Main index page
PageBlueprint.make({
name: 'index',
params: {
defaultPath: '/my-plugin',
loader: async () => import('./pages/IndexPage').then(m => <m.IndexPage />),
},
}),

// Detail page
PageBlueprint.make({
name: 'detail',
params: {
defaultPath: '/my-plugin/:id',
loader: async () => import('./pages/DetailPage').then(m => <m.DetailPage />),
},
}),

// Settings page
PageBlueprint.make({
name: 'settings',
params: {
defaultPath: '/my-plugin/settings',
loader: async () => import('./pages/SettingsPage').then(m => <m.SettingsPage />),
},
}),
],
});

Providing APIs​

import { ApiBlueprint } from '@backstage/frontend-plugin-api';
import { myApiRef, MyApiClient } from './api';

export const myApiExtension = ApiBlueprint.make({
name: 'my-api',
params: {
api: myApiRef,
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new MyApiClient({ discoveryApi, fetchApi }),
},
});

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
myPageExtension,
myApiExtension, // Provide API
],
});

Providing Nav Items​

import { NavItemBlueprint } from '@backstage/frontend-plugin-api';
import ExtensionIcon from '@material-ui/icons/Extension';

export const myNavItemExtension = NavItemBlueprint.make({
name: 'my-plugin',
params: {
title: 'My Plugin',
icon: ExtensionIcon,
routeRef: myRouteRef,
},
});

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
myPageExtension,
myNavItemExtension, // Add to sidebar
],
});

Providing Entity Cards​

import { createExtension, coreExtensionData } from '@backstage/frontend-plugin-api';

export const myEntityCardExtension = createExtension({
id: 'entity-card:my-plugin/overview',
attachTo: { id: 'page:catalog/entity', input: 'cards' },
output: {
element: coreExtensionData.reactElement,
},
factory: () => ({
element: <MyEntityCard />,
}),
});

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
myEntityCardExtension, // Show on entity pages
],
});

Plugin Configuration​

Plugins can be configured via app-config.yaml.

Reading Configuration in Components​

import { useApi, configApiRef } from '@backstage/core-plugin-api';

export const MyPage = () => {
const configApi = useApi(configApiRef);
const apiUrl = configApi.getString('myPlugin.apiUrl');
const refreshInterval = configApi.getOptionalNumber('myPlugin.refreshInterval') ?? 60;

// Use configuration...
};

Configuration in API Implementations​

export class MyApiClient implements MyApi {
private readonly baseUrl: string;
private readonly timeout: number;

constructor(options: {
configApi: ConfigApi;
fetchApi: FetchApi;
}) {
this.baseUrl = options.configApi.getString('myPlugin.apiUrl');
this.timeout = options.configApi.getOptionalNumber('myPlugin.timeout') ?? 30000;
}
}

Extension Configuration Schema​

import { createSchemaFromZod } from '@backstage/frontend-plugin-api';

export const myPageExtension = PageBlueprint.make({
name: 'index',
params: {
defaultPath: '/my-plugin',
loader: async () => <MyPage />,
},
configSchema: createSchemaFromZod((z) =>
z.object({
showWelcome: z.boolean().default(true),
itemsPerPage: z.number().default(20),
}),
),
});

App users can configure:

app:
extensions:
- page:my-plugin/index:
config:
showWelcome: false
itemsPerPage: 50

Frontend Modules​

Frontend modules extend existing plugins without modifying them.

What Are Frontend Modules?​

  • Add extensions to existing plugins
  • Override plugin defaults
  • Customize plugin behavior
  • No need to fork the plugin

Creating a Frontend Module​

import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { PageBlueprint } from '@backstage/frontend-plugin-api';

export const catalogCustomPage = createFrontendModule({
pluginId: 'catalog', // Extends catalog plugin
extensions: [
PageBlueprint.make({
name: 'custom-view',
params: {
defaultPath: '/catalog/custom',
loader: async () => <CustomCatalogView />,
},
}),
],
});

Module Export Structure​

// src/alpha.ts
export { catalogCustomPage as default } from './module';

Installing Frontend Modules​

import catalogCustomPage from './modules/catalog-custom-page/alpha';

createApp({
features: [
catalogCustomPage, // Install module
],
});

Or with auto-discovery:

app:
packages:
- all # Discovers modules automatically

Publishing Plugins​

Package Configuration​

Ensure your package.json is properly configured:

{
"name": "@backstage-community/plugin-my-plugin",
"version": "1.0.0",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/my-plugin/plugins/my-plugin"
},
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./package.json": "./package.json"
},
"files": [
"dist",
"config.d.ts"
],
"backstage": {
"role": "frontend-plugin"
}
}

Build Configuration​

{
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
}
}

Documentation​

Create comprehensive README.md:

# My Plugin

## Installation

```bash
yarn add @backstage-community/plugin-my-plugin

Configuration​

Add to your app-config.yaml:

myPlugin:
apiUrl: https://api.example.com

Usage​

New Frontend System​

The plugin is automatically discovered if you have plugin auto-discovery enabled:

app:
packages:
- all

Or manually install:

import myPlugin from '@backstage-community/plugin-my-plugin/alpha';

createApp({
features: [myPlugin],
});

Legacy Frontend System​

// packages/app/src/App.tsx
import { MyPage } from '@backstage-community/plugin-my-plugin';

const routes = (
<FlatRoutes>
<Route path="/my-plugin" element={<MyPage />} />
</FlatRoutes>
);

Features​

  • Feature 1
  • Feature 2

### Publishing to npm

```bash
# Build the plugin
yarn build

# Publish to npm
npm publish

# Or with Backstage CLI
yarn backstage-cli package publish

Best Practices​

1. Support Both Systems​

Support both legacy and new frontend systems during the transition period:

// src/index.ts - Legacy exports
export { MyPage, MyWidget } from './components';
export { myApiRef, type MyApi } from './api';

// src/alpha.ts - New system exports
export { myPlugin as default } from './plugin';
export { myPageExtension } from './extensions';

2. Lazy Load Components​

Always use dynamic imports for component loading:

// Good
loader: async () => {
const { MyPage } = await import('./components/MyPage');
return <MyPage />;
}

// Avoid
loader: async () => <MyPage /> // MyPage imported at top level

3. Provide Clear Extension Names​

// Good - Clear, descriptive names
PageBlueprint.make({ name: 'overview' })
PageBlueprint.make({ name: 'entity-detail' })
PageBlueprint.make({ name: 'settings' })

// Avoid - Generic names
PageBlueprint.make({ name: 'page1' })
PageBlueprint.make({ name: 'page2' })

4. Document Extensions​

/**
* Main overview page for My Plugin
*
* @alpha
* Extension ID: page:my-plugin/overview
* Default path: /my-plugin
*/
export const overviewPageExtension = PageBlueprint.make({
name: 'overview',
params: { /* ... */ },
});

5. Use Semantic Versioning​

  • Major: Breaking changes
  • Minor: New features, backward compatible
  • Patch: Bug fixes

6. Provide Migration Guide​

Help users migrate from legacy to new system:

## Migration from Legacy to New Frontend System

### Before (Legacy)

```typescript
import { MyPage } from '@backstage-community/plugin-my-plugin';

<Route path="/my-plugin" element={<MyPage />} />

After (New System)​

// Auto-discovered, or:
import myPlugin from '@backstage-community/plugin-my-plugin/alpha';
createApp({ features: [myPlugin] });

### 7. Test Plugin Installation

Test that your plugin works when installed in a fresh Backstage app:

```bash
# Create test app
npx @backstage/create-app@latest --next test-app

# Add your plugin
cd test-app
yarn add @backstage-community/plugin-my-plugin

# Test auto-discovery
yarn dev

8. Minimize Dependencies​

Only include necessary dependencies:

{
"dependencies": {
"@backstage/core-components": "^0.15.0",
"@backstage/core-plugin-api": "^1.10.0",
"@backstage/frontend-plugin-api": "^0.9.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}

Avoid heavy libraries if possible.


Common Patterns​

Pattern 1: Plugin with Multiple Features​

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
// Pages
overviewPage,
detailPage,
settingsPage,

// Navigation
navItem,

// APIs
myApi,

// Entity extensions
entityCard,
entityContent,
],
});

Pattern 2: Configurable Plugin​

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
PageBlueprint.make({
name: 'index',
params: {
defaultPath: '/my-plugin',
loader: async () => <MyPage />,
},
configSchema: createSchemaFromZod((z) =>
z.object({
features: z.object({
analytics: z.boolean().default(true),
notifications: z.boolean().default(false),
}),
}),
),
}),
],
});

Pattern 3: Plugin with Optional Extensions​

export const myPlugin = createFrontendPlugin({
id: 'my-plugin',
extensions: [
// Always included
mainPage,

// Conditionally included
process.env.NODE_ENV === 'development' && devToolsPage,

// Optional based on config
experimentalFeature,
].filter(Boolean),
});

Pattern 4: Plugin Extending Another Plugin​

// Extending the catalog plugin
export const catalogEnhancementsModule = createFrontendModule({
pluginId: 'catalog',
extensions: [
// Add custom entity card
createExtension({
id: 'entity-card:catalog-enhancements/metrics',
attachTo: { id: 'page:catalog/entity', input: 'cards' },
output: { element: coreExtensionData.reactElement },
factory: () => ({ element: <MetricsCard /> }),
}),
],
});

Troubleshooting​

Plugin Not Discovered​

Symptom: Plugin not loading automatically

Solutions:

  1. Check exports field includes ./alpha
  2. Verify backstage.role: "frontend-plugin" in package.json
  3. Ensure plugin is installed in package.json dependencies
  4. Check app-config.yaml has app.packages: [all]
  5. Restart dev server

Extension Not Appearing​

Symptom: Extension registered but not visible

Solutions:

  1. Check extension is in plugin's extensions array
  2. Verify extension ID doesn't conflict
  3. Check attachTo points to valid parent
  4. Ensure extension isn't disabled in app-config.yaml

Type Errors with Alpha Exports​

Symptom: TypeScript errors when importing /alpha

Solutions:

  1. Add ./alpha to exports in package.json
  2. Ensure alpha.ts exists and exports plugin
  3. Check TypeScript version compatibility
  4. Verify @backstage/frontend-plugin-api version

Summary​

Key Takeaways:

  1. Plugins are collections of extensions - Pages, APIs, nav items, etc.
  2. Use /alpha exports - Separate new frontend system from legacy
  3. Support auto-discovery - Configure package.json correctly
  4. Provide clear documentation - Help users install and configure
  5. Test thoroughly - Test in fresh Backstage apps
  6. Follow semantic versioning - Help users manage updates

Next Steps:


Navigation: