The Backstage App Repository and Wiring a Plugin
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
- Write
/root/cba-app/package.json—private: true, two entriespackages/*andplugins/*inworkspaces.packages, and four entriesdev,build:all,tsc, andtest:allinscripts.build:allis a command that starts withbackstage-cli repo build, andtest:allis a command that starts withbackstage-cli repo test. - Write
/root/cba-app/packages/app/package.json—name: app,backstage.role: frontend, andscripts.startasbackstage-cli package start. Also write/root/cba-app/packages/backend/package.json—name: backend,backstage.role: backend,mainas a path that starts withdist/, andscripts.startas the same command. - Write
/root/cba-app/packages/backend/src/index.ts— importcreateBackendfrom@backstage/backend-defaultsand call it, and writebackend.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. Callbackend.start()at the end, and leave no traces of the old backend style such ascreateRouter,PluginEnvironment, orapiRouter. - Write
/root/cba-app/plugins/oncall/package.json—name: @internal/plugin-oncall,backstage.role: frontend-plugin,sideEffects: false, and@backstage/core-plugin-apiindependencies. Also write/root/cba-app/plugins/oncall-backend/package.json—name: @internal/plugin-oncall-backend,backstage.role: backend-plugin,mainas a path that starts withdist/, and@backstage/backend-plugin-apiindependencies. - Write three files under
/root/cba-app/plugins/oncall/src/.routes.tscreates and exportsrootRouteRefwithid: 'oncall'usingcreateRouteRef.plugin.tsimports that reference from./routes, createsoncallPluginwithcreatePlugin, and createsOncallPagewithcreateRoutableExtension, hookingmountPoint: rootRouteRef. Do not callcreateRouteRefagain insideplugin.ts.index.tsre-exports the two,oncallPluginandOncallPage. - Write
/root/cba-app/packages/app/src/components/catalog/EntityPage.tsx— importOncallPagefrom@internal/plugin-oncall, and inside theisKind('component')condition attach a tab withpath="/oncall"andtitle="On-call"usingEntityLayout.Route. Also write/root/cba-app/packages/app/src/App.tsx— import the same plugin and hook<OncallPage />as the element on the route withpath="/oncall". - In
/root/cba-app/packages.txt, write the name and role of every package underpackages/andplugins/in the format이름=역할(the placeholders are the package name and the role), one per line, sorted and without duplicates. - Write
/root/cba-app/.github/workflows/ci.yaml— apull_requesttrigger,jobs.build.runs-on: ubuntu-latest, and exactly sixsteps. The first isactions/checkout, the second isactions/setup-node, and the next four, in order, runyarn install --immutable,yarn tsc,yarn build:all, andyarn test:allas run commands.
Notes
- Use four role values:
frontend,backend,frontend-plugin, andbackend-plugin. - A backend package with
-module-in its name is a piece that plugs into the extension point of the plugin it pairs with. - Common mistake 1: attaching only the tab and leaving out the app route. There is no error on screen, so the cause is hard to find.
- Common mistake 2: creating the route reference inside
plugin.ts. The files were split to prevent a circular import, so this defeats the purpose. - Common mistake 3: dropping
--immutablein CI. If the lock file quietly changes, a commit that passed yesterday builds differently today.
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.