Skip to main content
Version: v1.1.x

Installing into an existing Backstage app

This guide walks through installing the OpenChoreo plugin set into a Backstage app you already own. It assumes a standard Backstage workspace produced by npx @backstage/create-app with the New Backend System (the default since Backstage 1.32).

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 capability on top of Core; you can install any subset independently.

info

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.

1. Prerequisites​

  • A Backstage workspace on the supported Backstage version. This guide pins to Backstage 1.43.3.
  • The workspace must be scaffolded with the legacy frontend system: npx @backstage/create-app@latest --legacy. Current create-app versions default to the new declarative frontend system (createApp({ features: [...] }) in App.tsx), which does not match the Β§4.3 edits below β€” those edits rely on the legacy structure (apis.ts, createApp({ apis, plugins, bindRoutes }), and components/catalog/EntityPage.tsx).
  • Node.js 20 or 22, Yarn 4.4.1.
  • Access to a running OpenChoreo control plane (local k3d or a deployed cluster).
  • OAuth client credentials for the OpenChoreo Identity Provider (used by the catalog sync and user sign-in). On k3d the helm chart pre-seeds these; for a deployed cluster see Identity configuration.

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. Without these pins, transitive @backstage/* deps drift to newer minor versions and you will hit missing serviceRef{alpha.core.metrics} and similar errors at startup.

{
"resolutions": {
"@types/react": "18.3.26",
"@types/react-dom": "18.3.7",
"csstype": "3.1.3",
"@backstage/backend-defaults": "0.12.1",
"@backstage/backend-plugin-api": "1.4.3",
"@backstage/catalog-model": "1.7.5",
"@backstage/cli": "0.34.3",
"@backstage/config": "1.3.4",
"@backstage/core-plugin-api": "1.11.0",
"@backstage/plugin-catalog-backend": "3.1.1",
"@backstage/plugin-catalog-backend-module-logs": "0.1.14",
"@backstage/plugin-catalog-node": "1.19.0",
"@backstage/plugin-permission-backend": "0.7.4",
"@backstage/plugin-permission-node": "0.10.4",
"@backstage/plugin-scaffolder-backend": "2.2.1",
"@backstage/plugin-scaffolder-node": "0.11.1"
}
}

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.43.3

…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): add to your workspace .yarnrc.yml:

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.

npm / Yarn Classic: add to your workspace .npmrc:

@openchoreo:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}

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.

Why sign-in is part of Core

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 (packages/app):

yarn workspace app add \
@openchoreo/backstage-design-system@^1.1.0 \
@openchoreo/backstage-plugin-common@^1.1.0 \
@openchoreo/backstage-plugin-react@^1.1.0 \
@openchoreo/backstage-plugin@^1.1.0 \
@material-ui/lab@4.0.0-alpha.61

In your backend workspace (packages/backend):

yarn workspace backend add \
@openchoreo/openchoreo-client-node@^1.1.0 \
@openchoreo/openchoreo-auth@^1.1.0 \
@openchoreo/backstage-plugin-common@^1.1.0 \
@openchoreo/backstage-plugin-catalog-backend-module@^1.1.0 \
@openchoreo/backstage-plugin-permission-backend-module-openchoreo-policy@^1.1.0 \
@openchoreo/backstage-plugin-scaffolder-backend-module@^1.1.0 \
@openchoreo/backstage-plugin-backend@^1.1.0 \
@openchoreo/backstage-plugin-auth-backend-module-openchoreo-auth@^1.1.0
Pinning all @openchoreo/* to the same minor

The 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.1.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​

Edit packages/backend/src/index.ts:

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();

// Service factories required by the OpenChoreo plugins.
// 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();
},
}),
);

// ... your existing backend.add(...) lines ...

// Sign-in: register OpenChoreo as an OIDC provider.
backend.add(import("@backstage/plugin-auth-backend"));
backend.add(OpenChoreoAuthModule);
backend.add(import("@backstage/plugin-auth-backend-module-guest-provider"));

// Replace `@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("@backstage/plugin-permission-backend"));
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​

Five sub-steps, all in packages/app/src/. Steps 4.3.1, 4.3.3, and 4.3.4 are also prerequisites for the opt-in sections that follow.

4.3.1 Register the plugin​

Backstage's plugin auto-discovery walks the JSX tree of the rendered app to find plugins. Plugins whose only mount points are EntityLayout.Route tabs inside EntitySwitch.Case branches are not discovered at boot (those branches only render once an entity is loaded), so their API factories are never registered. Register choreoPlugin explicitly in packages/app/src/App.tsx:

packages/app/src/App.tsx
import { choreoPlugin } from '@openchoreo/backstage-plugin';

const app = createApp({
apis,
plugins: [choreoPlugin], // <-- add this; opt-in sections will append more
bindRoutes({ bind }) { ... },
});

4.3.2 Wire OpenChoreo sign-in​

Create packages/app/src/apis/authRefs.ts:

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:

packages/app/src/apis.ts
import {
AnyApiFactory,
configApiRef,
createApiFactory,
discoveryApiRef,
oauthRequestApiRef,
} from "@backstage/core-plugin-api";
import { OAuth2 } from "@backstage/core-app-api";
import { openChoreoAuthApiRef } from "./apis/authRefs";

export { openChoreoAuthApiRef };

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"],
}),
}),
];

Swap the SignInPage component in packages/app/src/App.tsx for a feature-gated wrapper. This lets you toggle sign-in via openchoreo.features.auth.enabled without changing code β€” when your cluster runs with auth off, it falls through to guest sign-in:

packages/app/src/App.tsx
import { SignInPage } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { openChoreoAuthApiRef } from './apis/authRefs';

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,
}}
/>
);
}

const app = createApp({
apis,
plugins: [...],
components: { SignInPage: DynamicSignInPage }, // <-- here
// ...
});
Backend and frontend gates must agree

openchoreo.features.auth.enabled is read in both places. If the backend module sees it as true but the SignInPage 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 Override fetchApi to inject the IDP token​

The OpenChoreo backend modules read an x-openchoreo-token header from incoming requests, populated by createIdpTokenHeaderMiddleware() you wired in 4.2 Wire the backend. Backstage's default fetchApi does not know about that header β€” it only injects the Backstage identity token in Authorization. Without overriding fetchApi, every tab data fetch goes out without the IDP token and the backend responds 401.

Create packages/app/src/apis/OpenChoreoFetchApi.ts:

packages/app/src/apis/OpenChoreoFetchApi.ts
import {
ConfigApi,
FetchApi,
IdentityApi,
OAuthApi,
} from "@backstage/core-plugin-api";

const OPENCHOREO_TOKEN_HEADER = "x-openchoreo-token";
const DIRECT_MODE_HEADER = "x-openchoreo-direct";

export class OpenChoreoFetchApi implements FetchApi {
private readonly authEnabled: boolean;

constructor(
private readonly identityApi: IdentityApi,
private readonly oauthApi: OAuthApi,
configApi: ConfigApi,
) {
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);

// Direct mode: caller wants to bypass the Backstage backend and hit an
// external API directly. Strip the signal header; put IDP token in
// Authorization instead of x-openchoreo-token.
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 });
}
}

Register it against fetchApiRef in packages/app/src/apis.ts. The factory must come after the openChoreoAuthApiRef factory (it depends on it):

packages/app/src/apis.ts
import { fetchApiRef, identityApiRef } from "@backstage/core-plugin-api";
import { OpenChoreoFetchApi } from "./apis/OpenChoreoFetchApi";

export const apis: AnyApiFactory[] = [
// ... existing factories, including the openChoreoAuthApiRef one from 4.3.2 ...
createApiFactory({
api: fetchApiRef,
deps: {
identityApi: identityApiRef,
oauthApi: openChoreoAuthApiRef,
configApi: configApiRef,
},
factory: ({ identityApi, oauthApi, configApi }) =>
new OpenChoreoFetchApi(identityApi, oauthApi, configApi),
}),
];

4.3.4 Override permissionApi to inject the IDP token​

Backstage's default PermissionClient uses cross-fetch directly and bypasses fetchApi entirely β€” so even with the OpenChoreoFetchApi step above, permission /authorize requests would still miss the x-openchoreo-token header. The Deploy tab's env-scoped permission hooks (and similar hooks in the opt-in tab packs) depend on this.

Create packages/app/src/apis/OpenChoreoPermissionApi.ts:

packages/app/src/apis/OpenChoreoPermissionApi.ts
import { Config } from "@backstage/config";
import {
DiscoveryApi,
IdentityApi,
OAuthApi,
} from "@backstage/core-plugin-api";
import { AuthorizeResult } from "@backstage/plugin-permission-common";

const OPENCHOREO_TOKEN_HEADER = "x-openchoreo-token";

export 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];
}
}

Register against permissionApiRef in apis.ts:

packages/app/src/apis.ts
import { permissionApiRef } from "@backstage/plugin-permission-react";
import { OpenChoreoPermissionApi } from "./apis/OpenChoreoPermissionApi";

export const apis: AnyApiFactory[] = [
// ... existing factories, including the fetchApiRef one from 4.3.3 ...
createApiFactory({
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,
}),
}),
];

This factory short-circuits to ALLOW when permission.enabled: false, so installing it before you turn on the permission framework is safe.

4.3.5 Add the entity-page routes​

In packages/app/src/components/catalog/EntityPage.tsx, import the Core tab components and add them to the relevant entity layouts:

packages/app/src/components/catalog/EntityPage.tsx
import {
Environments,
CellDiagram,
ResourceDefinitionTab,
} from '@openchoreo/backstage-plugin';

// In your componentEntityPage / defaultEntityPage <EntityLayout> blocks:
<EntityLayout.Route path="/definition" title="Definition">
<ResourceDefinitionTab />
</EntityLayout.Route>
<EntityLayout.Route path="/environments" title="Deploy">
<Environments />
</EntityLayout.Route>

// In your systemPage <EntityLayout> block:
<EntityLayout.Route path="/definition" title="Definition">
<ResourceDefinitionTab />
</EntityLayout.Route>
<EntityLayout.Route path="/cell" title="Cell">
<CellDiagram />
</EntityLayout.Route>

// In your domainPage <EntityLayout> block:
<EntityLayout.Route path="/definition" title="Definition">
<ResourceDefinitionTab />
</EntityLayout.Route>

OpenChoreo synced components have type strings like deployment/web-application and deployment/service. The default componentPage EntitySwitch only routes service/website to the page that includes Environments β€” add the Environments route to defaultEntityPage too if you want it on every component kind.

4.4 Configure app-config​

Add an openchoreo block to your app-config.yaml (or app-config.local.yaml for development).

Feature flags must mirror the OpenChoreo cluster

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.

app-config.local.yaml
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:

app-config.local.yaml (k3d-only)
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:

  1. Backend logs show Plugin initialization complete for openchoreo, catalog, permission, auth.
  2. After ~30s: Successfully processed N entities (X domains, Y systems, Z components, ...).
  3. Browser β†’ http://localhost:3000 β†’ sign-in page shows "Sign in using OpenChoreo." Click through and land in the catalog.
  4. http://localhost:3000/catalog?filters[kind]=domain β†’ see your OpenChoreo namespaces.
  5. Click into any project (kind=system) β†’ Cell and Definition tabs are present and render real data.
  6. Click into any component β†’ Deploy and Definition tabs are present and render real data.

If you get a 401 on tab data fetches, you skipped 4.3.3 (OpenChoreoFetchApi). If you get "No permissions" on Deploy, you skipped 4.3.4 (OpenChoreoPermissionApi). See Troubleshooting for the full failure-mode index.


5. Add Observability tabs (optional)​

Adds Logs, Metrics, Alerts 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.1.0
yarn workspace backend add @openchoreo/backstage-plugin-openchoreo-observability-backend@^1.1.0

5.2 Wire the backend​

Append to packages/backend/src/index.ts (after the Core wiring):

backend.add(
import("@openchoreo/backstage-plugin-openchoreo-observability-backend"),
);

5.3 Wire the frontend​

Register the plugin (append to the plugins: [...] array you set up in 4.3.1):

packages/app/src/App.tsx
import { openchoreoObservabilityPlugin } from '@openchoreo/backstage-plugin-openchoreo-observability';

plugins: [
choreoPlugin,
openchoreoObservabilityPlugin, // <-- add
],

Add the observability routes to packages/app/src/components/catalog/EntityPage.tsx:

packages/app/src/components/catalog/EntityPage.tsx
import {
ObservabilityMetrics,
ObservabilityAlerts,
ObservabilityRuntimeLogs,
ObservabilityProjectRuntimeLogs,
ObservabilityTraces,
ObservabilityProjectIncidents,
ObservabilityRCA,
ObservabilityCostAnalysis,
} from '@openchoreo/backstage-plugin-openchoreo-observability';

// Component pages (componentEntityPage / defaultEntityPage):
<EntityLayout.Route path="/runtime-logs" title="Logs">
<ObservabilityRuntimeLogs />
</EntityLayout.Route>
<EntityLayout.Route path="/metrics" title="Metrics">
<ObservabilityMetrics />
</EntityLayout.Route>
<EntityLayout.Route path="/alerts" title="Alerts">
<ObservabilityAlerts />
</EntityLayout.Route>

// System page (systemPage):
<EntityLayout.Route path="/logs" title="Logs">
<ObservabilityProjectRuntimeLogs />
</EntityLayout.Route>
<EntityLayout.Route path="/traces" title="Traces">
<ObservabilityTraces />
</EntityLayout.Route>
<EntityLayout.Route path="/incidents" title="Incidents">
<ObservabilityProjectIncidents />
</EntityLayout.Route>
<EntityLayout.Route path="/rca-reports" title="RCA Reports">
<ObservabilityRCA />
</EntityLayout.Route>
<EntityLayout.Route path="/cost-analysis" title="Cost Analysis">
<ObservabilityCostAnalysis />
</EntityLayout.Route>

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, Metrics, Alerts 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.1.0
yarn workspace backend add @openchoreo/backstage-plugin-openchoreo-ci-backend@^1.1.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​

Register the plugin (append to the plugins: [...] array):

packages/app/src/App.tsx
import { openchoreoCiPlugin } from '@openchoreo/backstage-plugin-openchoreo-ci';

plugins: [
choreoPlugin,
openchoreoCiPlugin, // <-- add
],

Add the Build route to EntityPage.tsx:

packages/app/src/components/catalog/EntityPage.tsx
import { Workflows } from "@openchoreo/backstage-plugin-openchoreo-ci";

// In your componentEntityPage / defaultEntityPage:
<EntityLayout.Route path="/workflows" title="Build">
<Workflows />
</EntityLayout.Route>;

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. 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.1.0
yarn workspace backend add @openchoreo/backstage-plugin-openchoreo-workflows-backend@^1.1.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​

Register the plugin:

packages/app/src/App.tsx
import { openchoreoWorkflowsPlugin } from '@openchoreo/backstage-plugin-openchoreo-workflows';

plugins: [
choreoPlugin,
openchoreoWorkflowsPlugin, // <-- add
],

Mount the standalone Workflows page in your app's FlatRoutes. The plugin exports GenericWorkflowsPage β€” a routable extension whose API factories are registered through openchoreoWorkflowsPlugin:

packages/app/src/App.tsx
import { Route } from 'react-router-dom';
import { GenericWorkflowsPage } from '@openchoreo/backstage-plugin-openchoreo-workflows';

const routes = (
<FlatRoutes>
{/* ... your existing routes ... */}
<Route path="/workflows" element={<GenericWorkflowsPage />} />
</FlatRoutes>
);

Optionally add a sidebar entry in packages/app/src/components/Root/Root.tsx so users can navigate to it:

packages/app/src/components/Root/Root.tsx
import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';

<SidebarItem icon={PlayCircleOutlineIcon} to="workflows" text="Workflows" />

7.4 Verify​

Restart yarn start. Navigate to http://localhost:3000/workflows β†’ see the org-level workflow list.


8. Remove the notifications plugin (if present)​

The default @backstage/create-app template wires @backstage/plugin-notifications into both the sidebar (NotificationsSidebarItem) and routes (<NotificationsPage />). The version of that plugin published when this guide was tested expects toastApiRef and several components (Tag, Dialog, DialogHeader, ...) from @backstage/ui / @backstage/frontend-plugin-api that are not present in the 1.43.3 release line we pin. The sidebar item crashes the whole app shell with Cannot read properties of undefined (reading 'id').

Delete the import and JSX usage from packages/app/src/components/Root/Root.tsx:

// Remove:
// import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
// <NotificationsSidebarItem />

…and from packages/app/src/App.tsx:

// Remove:
// import { NotificationsPage } from '@backstage/plugin-notifications';
// <Route path="/notifications" element={<NotificationsPage />} />

You can leave the @backstage/plugin-notifications package installed; it just shouldn't be referenced from the rendered tree.

Backend cleanup​

The create-app --legacy scaffold also registers four backend modules in packages/backend/src/index.ts that are not pinned by the compatibility matrix. They drift to versions outside the 1.43.3 release line and crash the backend at startup with TypeError: Cannot read properties of undefined (reading 'id') (see Troubleshooting β†’ Backend module startup fails).

Remove their backend.add(...) lines from packages/backend/src/index.ts:

// Remove:
// backend.add(import('@backstage/plugin-scaffolder-backend-module-notifications'));
// backend.add(import('@backstage/plugin-notifications-backend'));
// backend.add(import('@backstage/plugin-signals-backend'));
// backend.add(import('@backstage/plugin-mcp-actions-backend'));

Then drop the packages from packages/backend/package.json:

yarn workspace backend remove \
@backstage/plugin-scaffolder-backend-module-notifications \
@backstage/plugin-notifications-backend \
@backstage/plugin-signals-backend \
@backstage/plugin-mcp-actions-backend

The signals backend was paired with a <SignalsDisplay /> component in packages/app/src/App.tsx. Remove that too so the frontend doesn't reference a backend that no longer exists:

// Remove from packages/app/src/App.tsx:
// import { SignalsDisplay } from '@backstage/plugin-signals';
// <SignalsDisplay />

Troubleshooting​

If anything misbehaves at any step, see Troubleshooting for the full failure-mode index, indexed by exact error message.