Skip to main content
Version: Next

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 to createApp({ features: [...] }), and every OpenChoreo entity tab and overview card auto-mounts. No EntityPage.tsx edits. 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 heavy EntityPage.tsx customization 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.

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.

Tracking the upcoming 1.2.0 release

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-app scaffold ships Yarn 4.13.0 in .yarn/releases/).
  • 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.
Backstage version

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

package.json
{
"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:

.yarnrc.yml
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.

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:

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

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:

packages/app/src/App.tsx
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:

packages/app/src/App.tsx
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 SignInPageBlueprint extension that toggles between OpenChoreo OIDC sign-in and guest sign-in based on openchoreo.features.auth.enabled.
  • An ApiBlueprint extension that registers OpenChoreoFetchApi against fetchApiRef so every tab data fetch carries the x-openchoreo-token header.
  • An ApiBlueprint extension that registers OpenChoreoPermissionApi against permissionApiRef so 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.

packages/app/src/customAppModule.tsx
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,
}),
}),
],
});
Backend and frontend auth flags must agree

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

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 DIAGRAM 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, 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: [...] }):

packages/app/src/App.tsx
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: [...] }):

packages/app/src/App.tsx
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:

packages/app/src/App.tsx
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.

Choose one path

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:

packages/app/src/App.tsx (legacy host)
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:

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 { 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:

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.