Plugin System
Butler Portal plugins follow the standard Backstage plugin architecture. Each plugin is composed of up to three packages that work together: a frontend, a backend, and a common package for shared types.
Plugin Package Structure
Plugins vary in composition. Some have frontend, backend, and common packages. Others are frontend-only and rely on shared backends for data access.
plugins/
butler/ # Cluster management frontend
butler-backend/ # K8s proxy, WebSocket terminal, cluster API
workspaces/ # Chambers frontend (frontend-only, uses butler-backend)
registry/ # Keeper frontend
registry-backend/ # Keeper API and PostgreSQL storage
registry-common/ # Keeper shared types and permissions
pipeline/ # Herald frontend (React Flow, CodeMirror)
pipeline-backend/ # Herald API and Vector execution
pipeline-common/ # Herald shared types and permissions
Not every plugin needs all three packages. Chambers (workspaces) is a frontend-only plugin that communicates with the management cluster through the Butler backend. Keeper and Herald each have their own backends and common packages because they manage independent data stores and permissions.
Frontend Plugins
Frontend plugins are React components that mount into the Backstage app shell. Each frontend plugin exports a plugin object and one or more routable extensions.
Registration
Frontend plugins register with the Backstage app in packages/app/src/App.tsx:
import { ButlerPage } from '@internal/plugin-butler';
// In the app routes:
<Route path="/butler" element={<ButlerPage />} />
The plugin object created via createPlugin() declares the plugin's ID, API references, and any dependencies on other plugins.
API Communication
Frontend plugins communicate with their backends through the Backstage proxy or by using API refs. There are two patterns:
Proxy pattern: The frontend calls the Backstage backend proxy, which forwards requests to the plugin backend. This is the simpler approach and works well when the backend is colocated with the Backstage backend.
// Frontend API client using the proxy
const response = await fetch(`${baseUrl}/api/proxy/butler/clusters`);
API ref pattern: The frontend declares an API ref and provides a client implementation. The Backstage API system handles dependency injection and discovery.
import { createApiRef } from '@backstage/core-plugin-api';
export const butlerApiRef = createApiRef<ButlerApi>({
id: 'plugin.butler',
});
Backend Plugins
Backend plugins are Express routers that register with the Backstage backend. They handle HTTP requests, interact with databases, and call external services.
Registration
Backend plugins register with the Backstage backend in packages/backend/src/index.ts:
import { butlerPlugin } from '@internal/plugin-butler-backend';
// In the backend builder:
backend.add(butlerPlugin);
Router Structure
Each backend plugin exports a router factory that receives Backstage backend services (logger, database, config, permissions):
import { createRouter } from './router';
export const butlerPlugin = createBackendPlugin({
pluginId: 'butler',
register(env) {
env.registerInit({
deps: {
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
config: coreServices.rootConfig,
},
async init({ httpRouter, logger, config }) {
httpRouter.use(
await createRouter({ logger, config }),
);
},
});
},
});
External Service Communication
Backend plugins communicate with the Butler management cluster through the Backstage Kubernetes plugin. This provides authenticated access to the Kubernetes API server and Butler CRDs:
import { KubernetesClientProvider } from '@backstage/plugin-kubernetes-node';
// Use the Kubernetes client to list Workspace resources
const client = await kubernetesClientProvider.getClient(clusterName);
const workspaces = await client.listNamespacedCustomObject(
'butler.butlerlabs.dev',
'v1alpha1',
namespace,
'workspaces',
);
Common Packages
Common packages contain shared TypeScript types, constants, and utility functions used by both the frontend and backend packages of a plugin. They have no runtime dependencies on Backstage APIs.
// registry-common/src/types.ts
export interface Artifact {
id: string;
name: string;
type: ArtifactType;
version: string;
status: ArtifactStatus;
}
export type ArtifactType =
| 'terraform-module'
| 'helm-chart'
| 'opa-policy';
Both the frontend and backend import from the common package:
import { Artifact, ArtifactType } from '@internal/plugin-registry-common';
Plugin Discovery
Backstage uses a static plugin discovery model. Plugins are installed as npm packages and explicitly registered in the app and backend entry points. There is no dynamic plugin loading at runtime.
To add a new plugin to the Portal:
- Create the plugin packages under
plugins/in the monorepo. - Add the frontend plugin to
packages/app/src/App.tsx. - Add the backend plugin to
packages/backend/src/index.ts. - Add navigation entries to
packages/app/src/components/Root/Root.tsx.
See Also
- Architecture Overview for the high-level system diagram
- Contributing for step-by-step plugin development instructions
- Reference for plugin configuration options