How a ~106k-line legacy Backstage portal is being replaced by a slim core instance that consumes independently versioned plugin repositories
— the architecture, the organization-scoped entity and permission model, the phased execution, and the cutover plan.
Why the migration exists
The legacy portal grew into a single repository holding 24 plugins plus a 26–34k-line application monolith. Three hub files (App.tsx,
the backend index, and the permission policy) had to change for nearly every feature, producing structural merge conflicts and repeated
production and development outages. Overall statement coverage sat near 32% against an organizational hard requirement of 80%.
The structural root cause was modeling Business Units as catalog Domain entities, with a service that rewired Backstage internals and
entangled the GitHub backend, RBAC, and provisioning into one inseparable knot. The migration removes that layer entirely: the GitHub
organization becomes the first-class grouping, expressed as the entity namespace throughout the catalog.
Target architecture
Per-topic plugin repositoriesEight workspaces (grafana, github-org, rbac, terraform, dataproxy, infoblox, scorecards,
portal-home), each following community-plugins conventions: changesets, ADR011 package naming, a pinned Backstage version, and API
reports.
Slim core instanceThe refreshed instance runs the new frontend system (feature discovery over installed packages) and the new
backend system (one explicit registration per plugin). The app shell is roughly 241 lines, replacing the 34k-line monolith.
Release-based distributionEvery repository builds publish-ready packages; CI packs tarballs and attaches them to per-package
GitHub Releases that the instance consumes. Swapping to a private npm registry later is a configuration-only change.
Isolation rulesPlugin workspaces share no source code; cross-workspace needs become published common/node packages or HTTP
contracts. Custom annotation keys are standardized under a single organization-owned namespace.
The organization-scoped model
Business Unit → GitHub organizationThe Business-Unit hierarchy layer is dropped rather than ported. The GitHub organization,
lowercased, becomes the entity namespace, and a reviewed mapping table moves existing Business Units onto their organizations, with
unmapped strays failing loudly for manual disposition.
Two-tier RBACA config-declared admin team receives full access. Everyone else gets catalog reads filtered at the database level
by their organization memberships (plus entity ownership), can run scaffolder templates and use read-only plugins, and is denied other
writes.
Sign-in with organization claimsA custom GitHub sign-in resolver fetches the user's organizations with pagination (fixing a
truncation bug in an earlier attempt) and issues synthetic group claims that the permission policy consumes.
Business Units return, org-alignedA later decision brings Business Units back as one-to-one, organization-aligned Domain entities
with a sidebar tab — without resurrecting the hierarchy layer or the internals rewiring that caused the original problems.
Phased execution
Completed
Phases 0–1 — Foundation and the Grafana pilot
Completed August 9, 2026
The instance was bumped to the latest stable Backstage, release tooling and runbooks were written, and committed secrets were untracked. The
Grafana plugin family was then extracted into the first standalone repository and released, proving the full publish-and-consume loop end to
end. The pilot doubled as the calibration unit for every later estimate.
Completed
Phase 2 — github-org, the hardest split
Code complete August 9, 2026
The 14.5k-line legacy GitHub backend was split into five packages, with all 27 legacy endpoints dispositioned through a porting map.
Business-Unit semantics were removed entirely in favor of the organization model. The package family shipped with 488 tests and at least 80%
coverage each, and a tested catalog data-migration script (dry-run and write modes) handles the entity move. Two latent legacy bugs — an
unauthenticated pull-requests endpoint and inconsistent admin repository names — were found and fixed during the rewrite.
Completed
Phase 3 — RBAC overhaul
Code complete August 9, 2026
A clean-room build of the two-tier policy and the organization-claims sign-in resolver replaced the stock provider and the allow-all policy.
Useful pieces of a colleague's unmerged work-in-progress branch were harvested as reference after a documented harvest-versus-discard
analysis, so nothing of value was lost. The modules ship 45 tests at 97% coverage and were boot-verified in the instance.
Completed
Phases 4 and 5a — Terraform provisioning stack and shared types
Code complete August 10, 2026; released and wired in August 11
The largest workspace: 14 packages and roughly 75k lines including tests, spanning the Terraform backend and frontend, 25 scaffolder field
extensions, 66 scaffolder actions, and a provisioning frontend — 1,141 tests in the workspace. A small shared-types package published first
(phase 5a) eliminated a deep source import that would have broken on publish and unblocked cross-repository dependencies.
Ongoing
Phases 5–6 — Remaining plugin families and app decomposition
In flight as of August 13–14, 2026
The dataproxy family and the small infoblox module are the remaining ports, followed by decomposing what is left of the application monolith
(scorecards, tech radar, portal home) and distributing entity cards to their topic workspaces. Of the 36 planned packages, 28 were done with
5 in flight as of August 12. A 27-template update train converts scaffolder templates to the organization model, with every template writing
through pull requests only.
Planned
Phase 7 — Deployment, parallel run, and cutover
Scheduled August 14–18, 2026
The agreed sequence: a technical review workshop with the team on August 14 (sign-off from named reviewers is a hard prerequisite for any
switchover decision), GitOps rollout to a dedicated namespace in the existing Kubernetes cluster by August 15, a compressed page-by-page
parallel-run comparison on August 17, and the refreshed instance live at its own URL on August 18. Both instances coexist afterwards; there
is no legacy code freeze — every pull request merged to legacy is ported into the plugin repositories — and the DNS and auth switchover date
is decided only after the working solution passes review.
Quality and velocity results
CoverageEvery ported package ships with at least 80% statement coverage from day one, against a legacy baseline near 32%; several
packages sit in the 90s or at 100%.
TestsRoughly 1,675 new tests across the plugin repositories so far, in a codebase whose legacy counterpart largely lacked them.
Audited rewritesPorts are rewrites, not copies: unsafe casts, debug logging, and dead paths were removed, deviations are
documented per package, and two latent legacy bugs were fixed along the way.
ScaleAbout 110k lines of TypeScript now live in the new workspaces, replacing about 106k lines of legacy custom code — the growth
is the test suites the legacy code never had.
Schedule compressionThe original plan estimated 10–16 elapsed weeks. Measured throughput completed the first five phases in
roughly two and a half working days, leaving about one to one and a half engineering weeks of porting work.
The real bottleneckEngineering stopped being the schedule driver early. Decision gates and operations tasks — secret rotation,
repository remotes, OAuth scopes, GitHub App installs, and the parallel-run window — now set the pace.
Decision discipline
Twenty-seven numbered decisions govern the migration, each recorded with its date, rationale, and any later amendment. Highlights: one
repository per topic following community conventions; release-based distribution now with a registry as the future state; the organization
model replacing Business Units, then Business Units returning in a constrained org-aligned form; templates writing only through pull
requests; no legacy freeze, with merged legacy changes ported forward instead; and plugin repositories staying private with team access
granted through an admin group.
Conflicts between planning documents and reality were resolved in writing — for example, a permission module described in meeting material
turned out to exist only on an unmerged branch, and the documented resolution (a clean-room rebuild that harvests the good pieces) let the
original author verify nothing was lost.
Supporting sources: migration plan, decisions log, executive summary, work-done summary, and timeline documents from the migration
roundup, August 8–14, 2026.