Installing into an existing Backstage app
This guide walks through installing the OpenChoreo plugin set into a Backstage app you already own.
Two install paths are supported:
- New Frontend System (NFS) β the default scaffold from
npx @backstage/create-app@latest. Plugins are added as features tocreateApp({ features: [...] }), and every OpenChoreo entity tab and overview card auto-mounts. NoEntityPage.tsxedits. This is the path you should follow unless you have a reason not to. - Legacy frontend system β for hosts scaffolded with
--legacy, or older apps where you have heavyEntityPage.tsxcustomization you want to keep. See Section 9 β Legacy frontend system (fallback).
The guide is organized by feature. Sections 1β3 are setup. Section 4 (Core) is the only mandatory install β after finishing it you have a working OpenChoreo-aware Backstage with Domain/System/Component pages and the Cell, Deploy, and Definition tabs rendering real data. Sections 5β7 each add one optional tab pack on top of Core; you can install any subset independently.
OpenChoreo plugins are published to GitHub Packages under the @openchoreo scope. You install them with yarn add once your package manager is authenticated against https://npm.pkg.github.com β see Authenticate to GitHub Packages below.
The install commands on this page reference @openchoreo/<pkg>@^1.2.0, which will be the GA dist-tag of the next plugin release. While 1.2.0 is still under active development, install via the next dist-tag to get the latest prerelease today:
yarn workspace app add @openchoreo/backstage-plugin@next
Once 1.2.0 GA is announced, swap @next for @^1.2.0 to pin to the stable release.
1. Prerequisitesβ
- A Backstage workspace on the supported Backstage version. This guide pins to Backstage
1.51.0. - The workspace must be scaffolded with the default NFS scaffold:
npx @backstage/create-app@latest. (Do NOT pass--legacy. If you must stay on legacy, see Section 9.) - Node.js 20 or 22, Yarn 4.13.x (the current
create-appscaffold ships Yarn 4.13.0 in.yarn/releases/). - Access to a running OpenChoreo control plane (local
k3dor a deployed cluster). - OAuth client credentials for the OpenChoreo Identity Provider (used by the catalog sync and user sign-in). On
k3dthe helm chart pre-seeds these; for a deployed cluster see Identity configuration.
create-app@latest will likely give you Backstage 1.52.x or newer at the time you read this. After scaffolding, run yarn backstage-cli versions:bump --release 1.51.0 (covered in Section 2) to align with the tested combination.
2. Pin Backstage versionsβ
Add the following resolutions to your workspace package.json. They lock every @backstage/* package to the version line that the OpenChoreo plugins are tested against, and pin @mui/material to a version that satisfies all transitive consumers (without this @mui/material pin, material-ui-popup-state resolves to a nested copy of MUI 5.13.x that lacks the /version subpath and Rspack fails to compile).
{
"resolutions": {
"@types/react": "^18",
"@types/react-dom": "^18",
"@mui/material": "^5.18.0"
}
}
For the full pinned set see the compatibility matrix.
If your existing app is at a different Backstage release line, run:
yarn backstage-cli versions:bump --release 1.51.0
yarn install
β¦to align before adding the OpenChoreo packages.
3. Authenticate to GitHub Packagesβ
GitHub Packages requires authentication even for read:packages-only operations. Create a classic Personal Access Token with the read:packages scope, then wire it into your package manager.
Yarn 4 (Berry) β the current create-app scaffold's .yarnrc.yml enables a 3-day npm minimum-age gate that blocks newly published packages. Update .yarnrc.yml to both add the @openchoreo scope auth and pre-approve the scope so fresh OpenChoreo releases install immediately:
nodeLinker: node-modules
npmMinimalAgeGate: 3d
npmPreapprovedPackages:
- "@backstage/*"
- "@openchoreo/*"
yarnPath: .yarn/releases/yarn-4.13.0.cjs
npmScopes:
openchoreo:
npmRegistryServer: "https://npm.pkg.github.com"
npmAlwaysAuth: true
npmAuthToken: "${GITHUB_PACKAGES_TOKEN}"
β¦then export GITHUB_PACKAGES_TOKEN=<your-pat> before running yarn install. Berry expands the ${...} placeholder from the environment so the token never lands in the repo.
In CI, GitHub Actions can use the auto-issued GITHUB_TOKEN instead of a PAT, provided the workflow has permissions: { packages: read } and the running repo is in (or a fork of) an org the package is published from.
4. Core installβ
Required. Delivers OpenChoreo sign-in, catalog sync (Domain/System/Component entities), and the Cell, Deploy, and Definition tabs on every entity page.
After this section, a user signs in via the OpenChoreo IDP, lands in the catalog, and clicks into an entity to see live Cell/Deploy/Definition data. Sections 5β7 build on this foundation; do them in any order.
The Deploy and Cell tabs read from OpenChoreo APIs that require a user IDP token (not the Backstage identity token). That token only exists after the user signs in via the OpenChoreo IDP. The catalog provider uses client-credentials and doesn't need user sign-in, but the tabs on those entities do. So sign-in lives in Core, not in an opt-in section.
If your OpenChoreo cluster runs with features.auth.enabled: false (no IDP), the same wiring still works β DynamicSignInPage (below) falls through to guest sign-in. See the cluster-mirroring warning in section 4.4.
4.1 Install packagesβ
In your app workspace:
yarn workspace app add \
@openchoreo/backstage-design-system@^1.2.0 \
@openchoreo/backstage-plugin-common@^1.2.0 \
@openchoreo/backstage-plugin-react@^1.2.0 \
@openchoreo/backstage-plugin@^1.2.0 \
@material-ui/lab@4.0.0-alpha.61
In your backend workspace:
yarn workspace backend add \
@openchoreo/openchoreo-client-node@^1.2.0 \
@openchoreo/openchoreo-auth@^1.2.0 \
@openchoreo/backstage-plugin-common@^1.2.0 \
@openchoreo/backstage-plugin-catalog-backend-module@^1.2.0 \
@openchoreo/backstage-plugin-permission-backend-module-openchoreo-policy@^1.2.0 \
@openchoreo/backstage-plugin-scaffolder-backend-module@^1.2.0 \
@openchoreo/backstage-plugin-backend@^1.2.0 \
@openchoreo/backstage-plugin-auth-backend-module-openchoreo-auth@^1.2.0
@openchoreo/* to the same minorThe packages above are linked together by the OpenChoreo release process β they always release with matching versions. Pinning them all to the same ^x.y.0 caret keeps your install on one consistent release line. If you want to track the cutting edge, swap ^1.2.0 for the next dist-tag: yarn workspace app add @openchoreo/backstage-plugin@next (etc.). Prereleases are published under next; stable releases are under latest.
4.2 Wire the backendβ
The NFS scaffold's packages/backend/src/index.ts already wires scaffolder, search, techdocs, kubernetes, notifications, signals, and mcp-actions for you. Add the OpenChoreo modules on top β don't replace the file.
Edit packages/backend/src/index.ts:
import { createBackend } from "@backstage/backend-defaults";
import { rootHttpRouterServiceFactory } from "@backstage/backend-defaults/rootHttpRouter";
import {
immediateCatalogServiceFactory,
annotationStoreFactory,
} from "@openchoreo/backstage-plugin-catalog-backend-module";
import { createIdpTokenHeaderMiddleware } from "@openchoreo/openchoreo-auth";
import { OpenChoreoAuthModule } from "@openchoreo/backstage-plugin-auth-backend-module-openchoreo-auth";
const backend = createBackend();
// OpenChoreo service factories.
// AnnotationStore is shared between the catalog module and openchoreo-backend.
// ImmediateCatalogService lets scaffolder actions register entities synchronously.
backend.add(immediateCatalogServiceFactory);
backend.add(annotationStoreFactory);
// IDP token middleware: reads x-openchoreo-token from frontend requests and
// stashes it in AsyncLocalStorage. The permission policy and tab backends
// pull from that context to authorize the signed-in user against OpenChoreo.
// MUST be added before applyDefaults() so the context exists when downstream
// handlers run.
backend.add(
rootHttpRouterServiceFactory({
configure: ({ app, applyDefaults, middleware }) => {
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(createIdpTokenHeaderMiddleware());
applyDefaults();
},
}),
);
// ... keep all the existing scaffold backend.add(...) lines ...
// Sign-in: register OpenChoreo as an OIDC provider.
backend.add(OpenChoreoAuthModule);
// Replace the scaffold's `@backstage/plugin-permission-backend-module-allow-all-policy`
// with the OpenChoreo permission policy. The OpenChoreo policy delegates
// `openchoreo.*` permissions to the OpenChoreo authorization service and
// falls back to ALLOW for everything else, so it is composable with
// other host-app policies.
backend.add(
import("@openchoreo/backstage-plugin-permission-backend-module-openchoreo-policy"),
);
// IMPORTANT: register catalog-backend-module BEFORE openchoreo-backend.
// openchoreo-backend depends on the AnnotationStore initialized by the catalog module.
backend.add(import("@openchoreo/backstage-plugin-catalog-backend-module"));
backend.add(import("@openchoreo/backstage-plugin-backend"));
backend.add(import("@openchoreo/backstage-plugin-scaffolder-backend-module"));
backend.start();
If your existing index.ts registers @backstage/plugin-permission-backend-module-allow-all-policy, remove that line β only one permission policy can be active at a time.
4.3 Wire the frontendβ
Three sub-steps, all in packages/app/src/. Compared to the legacy install, you do not edit EntityPage.tsx β every Core OpenChoreo tab (Cell, Deploy, Definition) and overview card is contributed automatically by the plugin's /alpha export.
4.3.1 Add the OpenChoreo plugin to createAppβ
A fresh NFS scaffold's packages/app/src/App.tsx is roughly:
import { createApp } from "@backstage/frontend-defaults";
import catalogPlugin from "@backstage/plugin-catalog/alpha";
import { navModule } from "./modules/nav";
export default createApp({
features: [catalogPlugin, navModule],
});
Add the OpenChoreo plugin to the features array:
import { createApp } from "@backstage/frontend-defaults";
import catalogPlugin from "@backstage/plugin-catalog/alpha";
import openchoreoPluginAlpha from "@openchoreo/backstage-plugin/alpha";
import { navModule } from "./modules/nav";
import { customAppModule } from "./customAppModule";
export default createApp({
features: [
catalogPlugin,
openchoreoPluginAlpha,
navModule,
customAppModule, // see 4.3.2 below
],
});
That single import gives you the Cell, Deploy, and Definition entity tabs (auto-mounted on Domain / System / Component pages by kind) and the OpenChoreo overview cards (Project Contents, Deployment Pipeline, Deployments widget, etc.).
4.3.2 Author the customAppModuleβ
Create packages/app/src/customAppModule.tsx. It bundles three things into one frontend module:
- A
SignInPageBlueprintextension that toggles between OpenChoreo OIDC sign-in and guest sign-in based onopenchoreo.features.auth.enabled. - An
ApiBlueprintextension that registersOpenChoreoFetchApiagainstfetchApiRefso every tab data fetch carries thex-openchoreo-tokenheader. - An
ApiBlueprintextension that registersOpenChoreoPermissionApiagainstpermissionApiRefso the Deploy tab's env-scoped permission checks include the IDP token.
The OpenChoreoFetchApi and OpenChoreoPermissionApi classes are the same as the legacy install β they are not shipped by the OpenChoreo plugins; you copy them into your app.
import {
ApiBlueprint,
configApiRef,
createFrontendModule,
discoveryApiRef,
fetchApiRef,
identityApiRef,
oauthRequestApiRef,
useApi,
} from "@backstage/frontend-plugin-api";
import { SignInPageBlueprint } from "@backstage/plugin-app-react";
import { permissionApiRef } from "@backstage/plugin-permission-react";
import { SignInPage } from "@backstage/core-components";
import { OAuth2 } from "@backstage/core-app-api";
import {
ApiRef,
BackstageIdentityApi,
createApiRef,
OAuthApi,
ProfileInfoApi,
SessionApi,
} from "@backstage/core-plugin-api";
import { AuthorizeResult } from "@backstage/plugin-permission-common";
import type { Config } from "@backstage/config";
import type { DiscoveryApi, IdentityApi } from "@backstage/core-plugin-api";
import type { FetchApi } from "@backstage/frontend-plugin-api";
const OPENCHOREO_TOKEN_HEADER = "x-openchoreo-token";
const DIRECT_MODE_HEADER = "x-openchoreo-direct";
export const openChoreoAuthApiRef: ApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: "auth.openchoreo-auth",
});
class OpenChoreoFetchApi implements FetchApi {
private readonly authEnabled: boolean;
constructor(
private readonly identityApi: IdentityApi,
private readonly oauthApi: OAuthApi,
configApi: Config,
) {
this.authEnabled =
configApi.getOptionalBoolean("openchoreo.features.auth.enabled") ?? true;
this.fetch = this.fetch.bind(this);
}
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const headers = new Headers(init?.headers);
const isDirect = headers.has(DIRECT_MODE_HEADER);
if (isDirect) headers.delete(DIRECT_MODE_HEADER);
if (isDirect) {
if (this.authEnabled) {
try {
const idpToken = await this.oauthApi.getAccessToken();
if (idpToken) headers.set("Authorization", `Bearer ${idpToken}`);
} catch {
// No IDP token β proceed unauthenticated.
}
}
} else {
const { token: backstageToken } = await this.identityApi.getCredentials();
if (backstageToken)
headers.set("Authorization", `Bearer ${backstageToken}`);
if (this.authEnabled) {
try {
const idpToken = await this.oauthApi.getAccessToken();
if (idpToken) headers.set(OPENCHOREO_TOKEN_HEADER, idpToken);
} catch {
// No IDP token available β proceed with just the Backstage token.
}
}
}
return fetch(input, { ...init, headers });
}
}
class OpenChoreoPermissionApi {
private readonly enabled: boolean;
private readonly authEnabled: boolean;
private readonly discovery: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly oauthApi: OAuthApi;
constructor(options: {
config: Config;
discovery: DiscoveryApi;
identity: IdentityApi;
oauthApi: OAuthApi;
}) {
this.discovery = options.discovery;
this.identityApi = options.identity;
this.oauthApi = options.oauthApi;
this.enabled =
options.config.getOptionalBoolean("permission.enabled") ?? false;
this.authEnabled =
options.config.getOptionalBoolean("openchoreo.features.auth.enabled") ??
true;
}
async authorize(request: {
permission: { name: string };
resourceRef?: string;
}) {
if (!this.enabled) return { result: AuthorizeResult.ALLOW };
const permissionApiUrl = await this.discovery.getBaseUrl("permission");
const { token: backstageToken } = await this.identityApi.getCredentials();
let idpToken: string | undefined;
if (this.authEnabled) {
try {
idpToken = await this.oauthApi.getAccessToken();
} catch {
// No IDP token β proceed with just the Backstage token.
}
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (backstageToken) headers.Authorization = `Bearer ${backstageToken}`;
if (idpToken) headers[OPENCHOREO_TOKEN_HEADER] = idpToken;
const response = await fetch(`${permissionApiUrl}/authorize`, {
method: "POST",
headers,
body: JSON.stringify({
items: [{ id: crypto.randomUUID(), ...request }],
}),
});
if (!response.ok) {
throw new Error(`Permission request failed: ${response.statusText}`);
}
const data = await response.json();
return data.items[0];
}
}
function DynamicSignInPage(props: any) {
const configApi = useApi(configApiRef);
const authEnabled =
configApi.getOptionalBoolean("openchoreo.features.auth.enabled") ?? true;
if (!authEnabled) {
return <SignInPage {...props} auto providers={["guest"]} />;
}
return (
<SignInPage
{...props}
provider={{
id: "openchoreo-auth",
title: "OpenChoreo",
message: "Sign in using OpenChoreo",
apiRef: openChoreoAuthApiRef,
}}
/>
);
}
export const customAppModule = createFrontendModule({
pluginId: "app",
extensions: [
SignInPageBlueprint.make({
params: {
loader: async () => DynamicSignInPage,
},
}),
ApiBlueprint.make({
name: "openchoreo-auth",
params: (defineParams) =>
defineParams({
api: openChoreoAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
configApi,
provider: {
id: "openchoreo-auth",
title: "OpenChoreo",
icon: () => null,
},
environment: configApi.getOptionalString("auth.environment"),
defaultScopes: ["openid", "profile", "email"],
}),
}),
}),
ApiBlueprint.make({
name: "fetch-api",
params: (defineParams) =>
defineParams({
api: fetchApiRef,
deps: {
identityApi: identityApiRef,
oauthApi: openChoreoAuthApiRef,
configApi: configApiRef,
},
factory: ({ identityApi, oauthApi, configApi }) =>
new OpenChoreoFetchApi(identityApi, oauthApi, configApi),
}),
}),
ApiBlueprint.make({
name: "permission-api",
params: (defineParams) =>
defineParams({
api: permissionApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
oauthApi: openChoreoAuthApiRef,
},
factory: ({ configApi, discoveryApi, identityApi, oauthApi }) =>
new OpenChoreoPermissionApi({
config: configApi,
discovery: discoveryApi,
identity: identityApi,
oauthApi,
}) as any,
}),
}),
],
});
openchoreo.features.auth.enabled is read in both places (4.3.2 here and 4.2 on the backend). If the backend module sees it as true but DynamicSignInPage falls through to guest, the OpenChoreo authz API will reject the guest token. Set it once in app-config and let both sides read it.
4.3.3 Entity-page tabs β already doneβ
Under NFS, every Core OpenChoreo tab is contributed by openchoreoPluginAlpha as an EntityContentBlueprint extension. You do not need to edit EntityPage.tsx (the NFS scaffold does not even create one).
After Section 4.5 β Verify you should see:
- Domain pages:
OVERVIEW,TECHDOCS,DEFINITION. - System pages:
OVERVIEW,TECHDOCS,DEFINITION,CELL DIAGRAMβ plus auto-mounted overview cards (Project Contents, Deployment Pipeline, etc.). - Component pages:
OVERVIEW,TECHDOCS,APIS,DEFINITION,DEPLOYβ plus the Deployments widget on Overview.
If you need to override where a tab appears or hide one for a specific kind, see Entity views β customizing under NFS.
4.4 Configure app-configβ
Add an openchoreo block to your app-config.yaml (or app-config.local.yaml for development).
openchoreo.features.auth.enabled and openchoreo.features.authz.enabled are not independent installer defaults. They must match how the OpenChoreo cluster your Backstage instance is talking to was deployed:
| Cluster runs with⦠| Set in Backstage |
|---|---|
| auth + authz both off (local dev mode) | auth.enabled: false, authz.enabled: false |
| auth + authz both on (production mode) | auth.enabled: true, authz.enabled: true |
The in-tree OpenChoreo portal hides this knob because the Helm chart configures both halves in lockstep. External Backstage hosts installing the published @openchoreo/* plugins bypass the chart and must align by hand. Mismatch produces failure modes covered in Troubleshooting: the catalog provider 401s if auth disagrees, and tabs render "No permissions" if authz disagrees.
Check your cluster: look at the features block in the OpenChoreo helm values, or ask whoever deployed it.
auth:
environment: development
providers:
openchoreo-auth:
development:
clientId: ${OPENCHOREO_AUTH_CLIENT_ID}
clientSecret: ${OPENCHOREO_AUTH_CLIENT_SECRET}
# Set the OAuth2 endpoints explicitly. `metadataUrl` (OIDC discovery)
# is *not* used because the default OpenChoreo IDP (ThunderID) does not
# expose a reliable /.well-known/openid-configuration endpoint on k3d
# β uncomment it only if your IDP advertises one.
# metadataUrl: ${OPENCHOREO_AUTH_METADATA_URL}
authorizationUrl: ${OPENCHOREO_AUTH_AUTHORIZATION_URL}
tokenUrl: ${OPENCHOREO_AUTH_TOKEN_URL}
scope: "openid profile email"
guest:
dangerouslyAllowOutsideDevelopment: true
openchoreo:
baseUrl: ${OPENCHOREO_API_URL} # e.g. http://api.openchoreo.localhost:8080/api/v1
# OAuth2 client credentials used by the catalog provider for
# service-to-service auth (client_credentials flow). Required even when
# features.auth.enabled is false, because the catalog provider still needs
# to fetch namespaces/projects/components from the OpenChoreo API.
auth:
clientId: ${OPENCHOREO_CLIENT_ID}
clientSecret: ${OPENCHOREO_CLIENT_SECRET}
tokenUrl: ${OPENCHOREO_TOKEN_URL}
features:
# MIRROR THE CLUSTER (see warning above). Backend and frontend both read
# these β they must agree across the whole stack.
auth:
enabled: true
authz:
enabled: true
defaultOwner: openchoreo-users
schedule:
frequency: 30 # seconds between catalog provider runs
timeout: 120 # seconds for catalog provider timeout
permission:
enabled: true # required for the OpenChoreo permission policy to run
# The catalog must accept Domain entities from the OpenChoreo provider.
catalog:
rules:
- allow: [Component, System, Domain, API, Resource, Location, Group, User]
Local k3d quickstartβ
OpenChoreo's k3d helm install seeds known credentials. Skip the ${VAR} placeholders and hardcode these values to get sign-in working without touching env vars:
auth:
environment: development
providers:
openchoreo-auth:
development:
clientId: openchoreo-backstage-client
clientSecret: backstage-portal-secret
authorizationUrl: http://thunder.openchoreo.localhost:8080/oauth2/authorize
tokenUrl: http://thunder.openchoreo.localhost:8080/oauth2/token
scope: "openid profile email"
openchoreo:
baseUrl: http://api.openchoreo.localhost:8080/api/v1
auth:
clientId: openchoreo-backstage-client
clientSecret: backstage-portal-secret
tokenUrl: http://thunder.openchoreo.localhost:8080/oauth2/token
These are k3d dev cluster seed values, not real secrets β never use them against a production OpenChoreo install. For deployed clusters see Identity configuration.
4.5 Verifyβ
yarn start
Expected:
- Backend logs show
Plugin initialization completeforopenchoreo,catalog,permission,auth. - After ~30s:
Successfully processed N entities (X domains, Y systems, Z components, ...). - Browser β
http://localhost:3000β sign-in page shows "Sign in using OpenChoreo." Click through and land in the catalog. http://localhost:3000/catalog?filters[kind]=domainβ see your OpenChoreo namespaces.- Click into any project (
kind=system) β CELL DIAGRAM and DEFINITION tabs are present and render real data. - Click into any component β DEPLOY and DEFINITION tabs are present and render real data.
If you get a 401 on tab data fetches, your customAppModule's ApiBlueprint for fetchApiRef isn't being registered β check that customAppModule is in the features: [...] array in App.tsx. If you get "No permissions" on Deploy, same check for the permissionApiRef ApiBlueprint. See Troubleshooting for the full failure-mode index.
5. Add Observability tabs (optional)β
Adds Logs, Events, Metrics, Alerts, Wirelogs to Component pages and Logs, Traces, Incidents, RCA Reports, Cost Analysis to System pages. Built on top of Core.
Prerequisites: Section 4 (Core) complete and verified.
5.1 Install packagesβ
yarn workspace app add @openchoreo/backstage-plugin-openchoreo-observability@^1.2.0
yarn workspace backend add @openchoreo/backstage-plugin-openchoreo-observability-backend@^1.2.0
5.2 Wire the backendβ
Append to packages/backend/src/index.ts:
backend.add(
import("@openchoreo/backstage-plugin-openchoreo-observability-backend"),
);
5.3 Wire the frontendβ
Append the observability Alpha feature to createApp({ features: [...] }):
import openchoreoObservabilityPluginAlpha from "@openchoreo/backstage-plugin-openchoreo-observability/alpha";
export default createApp({
features: [
catalogPlugin,
openchoreoPluginAlpha,
openchoreoObservabilityPluginAlpha, // <-- add
navModule,
customAppModule,
],
});
No EntityPage.tsx edits. Every observability tab auto-mounts on the correct entity kind.
5.4 Configureβ
Append to the openchoreo.features block in app-config.local.yaml:
openchoreo:
features:
observability:
enabled: true
5.5 Verifyβ
Restart yarn start. Open any Component β the LOGS, EVENTS, METRICS, ALERTS, WIRELOGS tabs appear. Open any System β LOGS, TRACES, INCIDENTS, RCA REPORTS, COST ANALYSIS tabs appear. If a tab renders an empty state, the backend reached OpenChoreo but no data is available yet for that scope. If a tab errors, see Troubleshooting.
6. Add CI/Build tab (optional)β
Adds a Build tab to Component pages, showing workflow runs and a parameter-form trigger. Built on top of Core.
Prerequisites: Section 4 (Core) complete and verified.
6.1 Install packagesβ
yarn workspace app add @openchoreo/backstage-plugin-openchoreo-ci@^1.2.0
yarn workspace backend add @openchoreo/backstage-plugin-openchoreo-ci-backend@^1.2.0
6.2 Wire the backendβ
Append to packages/backend/src/index.ts:
backend.add(import("@openchoreo/backstage-plugin-openchoreo-ci-backend"));
6.3 Wire the frontendβ
Append the CI Alpha feature to createApp({ features: [...] }):
import openchoreoCiPluginAlpha from "@openchoreo/backstage-plugin-openchoreo-ci/alpha";
export default createApp({
features: [
catalogPlugin,
openchoreoPluginAlpha,
openchoreoCiPluginAlpha, // <-- add
navModule,
customAppModule,
],
});
6.4 Configureβ
Append to the openchoreo.features block:
openchoreo:
features:
workflows:
enabled: true
6.5 Verifyβ
Restart yarn start. Open any Component β the BUILD tab appears. It lists past workflow runs and (depending on permissions) offers a "Trigger build" form.
7. Add standalone Workflows page (optional)β
Adds an org-level /workflows page for generic (non-component-scoped) workflow templates and runs, plus a Runs tab on Workflow and ClusterWorkflow entities. Built on top of Core.
Prerequisites: Section 4 (Core) complete and verified.
7.1 Install packagesβ
yarn workspace app add @openchoreo/backstage-plugin-openchoreo-workflows@^1.2.0
yarn workspace backend add @openchoreo/backstage-plugin-openchoreo-workflows-backend@^1.2.0
7.2 Wire the backendβ
Append to packages/backend/src/index.ts:
backend.add(
import("@openchoreo/backstage-plugin-openchoreo-workflows-backend"),
);
7.3 Wire the frontendβ
Append the workflows Alpha feature:
import openchoreoWorkflowsPluginAlpha from "@openchoreo/backstage-plugin-openchoreo-workflows/alpha";
export default createApp({
features: [
catalogPlugin,
openchoreoPluginAlpha,
openchoreoWorkflowsPluginAlpha, // <-- add
navModule,
customAppModule,
],
});
The plugin contributes the standalone /workflows page as a PageBlueprint β no <Route> mount needed.
Optionally add a sidebar entry in packages/app/src/modules/nav/Sidebar.tsx so users can navigate to it.
7.4 Verifyβ
Restart yarn start. Navigate to http://localhost:3000/workflows β see the org-level workflow list.
8. Troubleshootingβ
If anything misbehaves at any step, see Troubleshooting for the full failure-mode index, indexed by exact error message.
9. Legacy frontend system (fallback)β
If your Backstage host is scaffolded with --legacy (or you have heavy EntityPage.tsx customization you want to keep), follow the legacy wiring below instead of Sections 4.3 through 7. Sections 1β3 and 4.1β4.2 (prereqs, resolutions, GitHub Packages auth, package install, backend wiring) are identical.
The legacy install registers each plugin's default export via createApp({ apis, plugins, bindRoutes }) and manually mounts every entity tab into components/catalog/EntityPage.tsx.
Do NOT mix the two. If you import openchoreoPluginAlpha from /alpha and also mount <Environments /> in EntityPage.tsx, you will see duplicate tabs (or the wrong tab will win depending on extension load order).
9.1 Wire the frontend (legacy)β
In packages/app/src/App.tsx:
import { choreoPlugin } from '@openchoreo/backstage-plugin';
const app = createApp({
apis,
plugins: [choreoPlugin], // <-- add; opt-in sections will append more
bindRoutes({ bind }) { ... },
});
Wire OpenChoreo sign-in by creating packages/app/src/apis/authRefs.ts:
import {
ApiRef,
BackstageIdentityApi,
createApiRef,
OAuthApi,
ProfileInfoApi,
SessionApi,
} from "@backstage/core-plugin-api";
export const openChoreoAuthApiRef: ApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: "auth.openchoreo-auth",
});
Register the OAuth2 factory in packages/app/src/apis.ts:
import { OAuth2 } from "@backstage/core-app-api";
import { openChoreoAuthApiRef } from "./apis/authRefs";
export const apis: AnyApiFactory[] = [
// ... your existing factories ...
createApiFactory({
api: openChoreoAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
configApi,
provider: {
id: "openchoreo-auth",
title: "OpenChoreo",
icon: () => null,
},
environment: configApi.getOptionalString("auth.environment"),
defaultScopes: ["openid", "profile", "email"],
}),
}),
];
The OpenChoreoFetchApi and OpenChoreoPermissionApi classes from Section 4.3.2 above are registered as legacy createApiFactory entries in apis.ts (same shape as upstream apiFactories). Pass components: { SignInPage: DynamicSignInPage } to createApp for the sign-in slot.
9.2 Mount entity tabsβ
Add the Core tab components to packages/app/src/components/catalog/EntityPage.tsx:
import {
Environments,
CellDiagram,
ResourceDefinitionTab,
} from '@openchoreo/backstage-plugin';
// componentEntityPage / defaultEntityPage:
<EntityLayout.Route path="/definition" title="Definition">
<ResourceDefinitionTab />
</EntityLayout.Route>
<EntityLayout.Route path="/environments" title="Deploy">
<Environments />
</EntityLayout.Route>
// systemPage:
<EntityLayout.Route path="/definition" title="Definition">
<ResourceDefinitionTab />
</EntityLayout.Route>
<EntityLayout.Route path="/cell" title="Cell">
<CellDiagram />
</EntityLayout.Route>
// domainPage:
<EntityLayout.Route path="/definition" title="Definition">
<ResourceDefinitionTab />
</EntityLayout.Route>
For the legacy opt-in tab packs (Observability / CI / Workflows), see Entity views β Legacy wiring example. The wiring is the same as the 1.1.x install guide β plugins: [..., openchoreoObservabilityPlugin] plus the per-tab <EntityLayout.Route> blocks.