TT Lab
Get started
Learn Learning paths Courses

CBA — Backstage Associate

The Backstage App Repository and Wiring a Plugin

Continue in TT Lab

Goal

Set up yourself the structure of the Backstage app repository that create-app produces, and create one internal plugin and wire it into both the frontend and the backend. The lab Pod has no internet access and cannot download node dependencies, so you do not build; you work only with the structure and the wiring.

Why it matters

Using Backstage means owning a monorepo of your own. So if you cannot answer "which file should I change?", you cannot customize anything, and there is no answer you can find on screen. Three things in particular repeatedly block people in practice. First, a single role marker in package.json decides how that package is built. Second, in the new backend system a plugin and a module are different things, and anything with module in its name does nothing without the plugin it pairs with. Third, if you attach only a tab to an entity page and do not register the app route, the tab shows but a blank screen appears when you click it, and no error appears either. This lab has you build those three places by hand.

Steps

  1. Write /root/cba-app/package.json — private: true, two entries packages/* and plugins/* in workspaces.packages, and four entries dev, build:all, tsc, and test:all in scripts. build:all is a command that starts with backstage-cli repo build, and test:all is a command that starts with backstage-cli repo test.
  2. Write /root/cba-app/packages/app/package.json — name: app, backstage.role: frontend, and scripts.start as backstage-cli package start. Also write /root/cba-app/packages/backend/package.json — name: backend, backstage.role: backend, main as a path that starts with dist/, and scripts.start as the same command.
  3. Write /root/cba-app/packages/backend/src/index.ts — import createBackend from @backstage/backend-defaults and call it, and write backend.add(import('...')) exactly seven times. The packages to register are @backstage/plugin-app-backend, @backstage/plugin-catalog-backend, @backstage/plugin-catalog-backend-module-github, @backstage/plugin-scaffolder-backend, @backstage/plugin-techdocs-backend, @backstage/plugin-auth-backend, and @backstage/plugin-auth-backend-module-github-provider. Call backend.start() at the end, and leave no traces of the old backend style such as createRouter, PluginEnvironment, or apiRouter.
  4. Write /root/cba-app/plugins/oncall/package.json — name: @internal/plugin-oncall, backstage.role: frontend-plugin, sideEffects: false, and @backstage/core-plugin-api in dependencies. Also write /root/cba-app/plugins/oncall-backend/package.json — name: @internal/plugin-oncall-backend, backstage.role: backend-plugin, main as a path that starts with dist/, and @backstage/backend-plugin-api in dependencies.
  5. Write three files under /root/cba-app/plugins/oncall/src/. routes.ts creates and exports rootRouteRef with id: 'oncall' using createRouteRef. plugin.ts imports that reference from ./routes, creates oncallPlugin with createPlugin, and creates OncallPage with createRoutableExtension, hooking mountPoint: rootRouteRef. Do not call createRouteRef again inside plugin.ts. index.ts re-exports the two, oncallPlugin and OncallPage.
  6. Write /root/cba-app/packages/app/src/components/catalog/EntityPage.tsx — import OncallPage from @internal/plugin-oncall, and inside the isKind('component') condition attach a tab with path="/oncall" and title="On-call" using EntityLayout.Route. Also write /root/cba-app/packages/app/src/App.tsx — import the same plugin and hook <OncallPage /> as the element on the route with path="/oncall".
  7. In /root/cba-app/packages.txt, write the name and role of every package under packages/ and plugins/ in the format 이름=역할 (the placeholders are the package name and the role), one per line, sorted and without duplicates.
  8. Write /root/cba-app/.github/workflows/ci.yaml — a pull_request trigger, jobs.build.runs-on: ubuntu-latest, and exactly six steps. The first is actions/checkout, the second is actions/setup-node, and the next four, in order, run yarn install --immutable, yarn tsc, yarn build:all, and yarn test:all as run commands.

Notes

Workspace root

Write /root/cba-app/package.json — private: true, two entries packages/* and plugins/* in workspaces.packages, and four entries dev, build:all, tsc, and test:all in scripts. build:all is a command that starts with backstage-cli repo build, and test:all is a command that starts with backstage-cli repo test.

The root package is not published. And to use an internal plugin without publishing it to a registry, the plugin directory must fall within the workspace paths.

Two apps and their roles

Write /root/cba-app/packages/app/package.json — name: app, backstage.role: frontend, and scripts.start as backstage-cli package start. Also write /root/cba-app/packages/backend/package.json — name: backend, backstage.role: backend, main as a path that starts with dist/, and scripts.start as the same command.

backstage-cli picks the build method by looking at a single role marker in package.json. The entry point of a backend package must point to the build output, not the source.

Wire up the new backend system

Write /root/cba-app/packages/backend/src/index.ts — import createBackend from @backstage/backend-defaults and call it, and write backend.add(import('...')) exactly seven times. The packages to register are @backstage/plugin-app-backend, @backstage/plugin-catalog-backend, @backstage/plugin-catalog-backend-module-github, @backstage/plugin-scaffolder-backend, @backstage/plugin-techdocs-backend, @backstage/plugin-auth-backend, and @backstage/plugin-auth-backend-module-github-provider. Call backend.start() at the end, and leave no traces of the old backend style such as createRouter, PluginEnvironment, or apiRouter.

The new backend system does not wire routers by hand. And anything with module in its name does not work alone, so the plugin it pairs with must be registered together.

Internal plugin packages

Write /root/cba-app/plugins/oncall/package.json — name: @internal/plugin-oncall, backstage.role: frontend-plugin, sideEffects: false, and @backstage/core-plugin-api in dependencies. Also write /root/cba-app/plugins/oncall-backend/package.json — name: @internal/plugin-oncall-backend, backstage.role: backend-plugin, main as a path that starts with dist/, and @backstage/backend-plugin-api in dependencies.

Internal plugins are not published to a registry, so use an internal scope name. A frontend plugin declares that it has no side effects so that the bundler can drop code that is not used.

Three plugin source files

Write three files under /root/cba-app/plugins/oncall/src/. routes.ts creates and exports rootRouteRef with id: 'oncall' using createRouteRef. plugin.ts imports that reference from ./routes, creates oncallPlugin with createPlugin, and creates OncallPage with createRoutableExtension, hooking mountPoint: rootRouteRef. Do not call createRouteRef again inside plugin.ts. index.ts re-exports the two, oncallPlugin and OncallPage.

There must be only one place that creates the route reference. For the page extension, hook that reference as the mount point.

Entity page tab and app route

Write /root/cba-app/packages/app/src/components/catalog/EntityPage.tsx — import OncallPage from @internal/plugin-oncall, and inside the isKind('component') condition attach a tab with path="/oncall" and title="On-call" using EntityLayout.Route. Also write /root/cba-app/packages/app/src/App.tsx — import the same plugin and hook <OncallPage /> as the element on the route with path="/oncall".

The place that attaches the tab and the place that registers the route are different files. If you do only one side, there is no error on screen but nothing appears.

Package inventory

In /root/cba-app/packages.txt, write the name and role of every package under packages/ and plugins/ in the format 이름=역할 (the placeholders are the package name and the role), one per line, sorted and without duplicates.

The role is a value each package declares for itself. Do not write it by hand; gather it by reading it from the files.

CI gate

Write /root/cba-app/.github/workflows/ci.yaml — a pull_request trigger, jobs.build.runs-on: ubuntu-latest, and exactly six steps. The first is actions/checkout, the second is actions/setup-node, and the next four, in order, run yarn install --immutable, yarn tsc, yarn build:all, and yarn test:all as run commands.

It must be a gate that runs before merging, and the order is type checking after installation. If the lock file can change in the installation step, the gate protects nothing.