Build and maintain Backstage backend plugins following best practices.
This skill provides specialized knowledge and workflows for building Backstage backend plugins. It guides the development of server-side functionality including REST/HTTP APIs, background jobs, data processing, and integrations.
Use this skill when creating server-side functionality for Backstage: REST/HTTP APIs, background jobs, data processing, or integrations.
Before building a backend plugin, clearly understand:
Load reference files as needed based on the plugin requirements:
For Core Services:
For MCP Actions:
actionsRegistryServiceRef)For Testing:
Follow the workflow below for implementation, referring to the reference files as needed.
Important Decisions:
⚠️ MCP Actions Key Point:
credentials in their handler; use these with permissions.authorize() for sensitive operations. Unlike HTTP routes, MCP actions don't have route-level auth policies, so explicit permission checks are critical.After implementing the plugin:
startTestBackendTestDatabasesyarn backstage-cli package test --coverage
Before publishing:
yarn new → backend‑plugin; the package lives in plugins/<id>-backend/.createBackendPlugin, declare dependencies via deps, and initialize in register(env).registerInit.coreServices.httpRouter. Backstage prefixes plugin routers with /api/<pluginId>.httpRouter.addAuthPolicy({ path, allow }) to allow unauthenticated endpoints like /health.coreServices.actionsRegistryServiceRef from @backstage/backend-plugin-api/alpha.permissions.authorize() with the credentials passed to the action handler.permissionsRegistry.addPermissions() at plugin init.Validate inputs at the edge using a schema (e.g., zod) before hitting DBs or external services:
import { z } from 'zod';
const querySchema = z.object({ q: z.string().min(1) });
router.get('/search', async (req, res, next) => {
const parsed = querySchema.safeParse(req.query);
if (!parsed.success) return res.status(400).json({ error: 'invalid query' });
try {
// ... perform work with parsed.data.q
res.json({ items: [] });
} catch (e) { next(e); }
});
Add a terminal error handler to your router and prefer structured logs with context:
import { errorHandler } from '@backstage/backend-common';
router.use(errorHandler());
Keep backends secure-by-default
Open only explicit paths with addAuthPolicy
For protected routes, extract credentials with httpAuth
Derive user/entity identity via userInfo when required
// Inside a route handler
const creds = await httpAuth.credentials(req, { allow: ['user', 'service'] });
const { userEntityRef } = await userInfo.getUserInfo(creds);
logger.info('request', { userEntityRef });
const knex = await database.getClient(); to get a database client.js) and export them in package.json (see the Core Services Reference for details)logger.child({ plugin: 'example' }) for traceabilitypermissions.authorize() with the credentials from the action handlerauth.getPluginRequestToken({ onBehalfOf: credentials }) for downstream API calls to preserve user identityactions.ts file for MCP action registrationget_example_items, create_example_itemInputError, NotAllowedError# From the repository root
# Non-interactive (for AI agents/automation)
yarn new --select backend-plugin --option pluginId=example --option owner=""
This creates plugins/example-backend/ using the New Backend System with createBackendPlugin.
src/plugin.ts — plugin + DI + routerimport { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
database: coreServices.database, // optional
httpAuth: coreServices.httpAuth, // optional
userInfo: coreServices.userInfo, // optional
},
async init({ httpRouter, logger, database, httpAuth, userInfo }) {
const router = await createRouter({ logger, database, httpAuth, userInfo });
httpRouter.use(router);
// Secure-by-default: open /health only
httpRouter.addAuthPolicy({ path: '/health', allow: 'unauthenticated' });
},
});
},
});
export { examplePlugin as default } from './plugin';
Key points: DI via deps, register routes with the plugin’s httpRouter, and export the plugin as the default. (Backstage)
src/service/router.ts — minimal Express routerimport express from 'express';
import type {
LoggerService,
DatabaseService,
HttpAuthService,
UserInfoService,
} from '@backstage/backend-plugin-api';
export interface RouterOptions {
logger: LoggerService;
database?: DatabaseService;
httpAuth?: HttpAuthService;
userInfo?: UserInfoService;
}
export async function createRouter(options: RouterOptions): Promise<express.Router> {
const { logger } = options;
const router = express.Router();
router.get('/health', (_req, res) => {
logger.info('health check');
res.json({ status: 'ok' });
});
return router;
}
In packages/backend/src/index.ts:
const backend = createBackend();
backend.add(import('@internal/plugin-example-backend'));
backend.start();
Now GET http://localhost:7007/api/example/health returns { "status": "ok" }. (Backstage)
coreServices.database to get a Knex client; create your own migrations and run them via your chosen process. (Backstage)coreServices.httpAuth + coreServices.userInfo to obtain the calling user and their entity refs when an endpoint needs identity. (Backstage)packages/backend/src/index.ts via backend.add(import('@internal/plugin-<id>-backend')).yarn start at the root). Then check:{ "status": "ok" }/health with addAuthPolicy.Use Backstage's CLI for tests and lints:
yarn backstage-cli package test
yarn backstage-cli package lint
yarn backstage-cli repo lint
Keep routers small (/service/router.ts), inject dependencies (DB, auth, clients) from plugin.ts, and avoid in-memory state (horizontally scalable).
| Problem | Solution | Reference |
|---|---|---|
404s under /api |
Remember Backstage prefixes plugin routers with /api/<pluginId> |
Backstage |
| Auth unexpectedly required | Backends are secure by default; open endpoints explicitly via httpRouter.addAuthPolicy |
Backstage |
| Tight coupling | Never call other backend code directly; communicate over the network or through well-defined services | Backstage |
Load these resources as needed during development:
actionsRegistryServiceRef from @backstage/backend-plugin-api/alpha)credentials and auth.getPluginRequestToken()permissions.authorize() for sensitive operationsstartTestBackendTestDatabases