Why a read-only Backstage catalog is a Git workflow decision
A regulated organization that needs every catalog change traceable to a reviewed commit should run its Backstage catalog as read-only and GitOps-managed. In this model, the catalog is a projection of files in Git: the Backstage setting catalog.readonly is switched on in configuration, entities enter only through static locations and providers that read from repositories, and adding or changing a component becomes a pull request that someone other than the author approves. Teams need this pattern when auditors ask who added a service, who changed its owner, and which review approved it, and when the answer must come from version control rather than from a database row written through the portal UI.
The decision is mostly a Git workflow decision, and it has consequences beyond the Backstage configuration file. Once catalog.readonly is set, any plugin or custom action that writes to the catalog through the API stops working, so the reader must inventory what currently writes to the catalog (scaffolder steps, custom entity providers, third-party plugins) before the switch. The reader should also decide where the descriptor files live, who may merge to those repositories, and whether the Git provider's branch protection and required reviews are enforced for the catalog repository. Confirm the exact read-only behavior for your Backstage release in the project documentation, because it changes between versions.
Backstage is licensed under Apache-2.0, so the catalog pattern carries no license cost to adopt. DeployOpen implements the read-only, GitOps-managed catalog inside the client's own infrastructure, including the Git repositories, identity sign-in, and the deployment pipeline that applies configuration, so the audit trail stays under the organization's control. Before scoping that work, the reader should confirm which Git platform hosts the descriptor repositories, which identity provider maps reviewers to groups, and which team owns the approval process for registration requests.
Which catalog ingestion options fit a GitOps model
This table compares the main ways entities reach the Backstage catalog and how each fits a Git-only change model. Read the runtime writes column first, because it shows which paths let someone change the catalog outside a pull request. Confirm exact config keys and deletion behavior against your Backstage version before you commit to a pattern.
| Option | Best fit | How deletion works | Runtime writes allowed |
|---|---|---|---|
| catalog.readonly set to true | Instances where every catalog change must pass through Git review | Entities leave only when their configured source stops providing them, so no one can delete through the UI | No |
| Static locations in app-config | A small, stable set of repositories or files that the platform team pins | Removing a location from app-config stops ingestion; confirm how your version treats entities it already produced | No |
| Registered locations through the UI or API | Early rollouts where teams self-register while you build review habits | Unregistering stops refresh, but entities it created can remain as orphans depending on orphan settings | Yes |
| Discovery entity providers | Many repositories or an inventory that already serves as the system of record | Records missing from the source are removed on a later run, governed by the provider's delete strategy | No |
| Discovery providers with readonly enabled | Large GitOps catalogs where Git and the source are the only authorities | Deletion follows the source only; API write paths are blocked, so teams must change the source instead | No |
How to switch the catalog to Git-only changes
This sequence moves the Backstage catalog from runtime edits to a single Git-driven path, so every entity change is reviewed, merged and traceable. Each step ends with a check that confirms the switch is working before the next one starts.
Inventory every writer to the catalog
Before changing anything, list each plugin, script, API client and user account that adds, edits or deletes entities at runtime. Check the installed plugins in your Backstage app, the catalog API calls in your custom code, and any automation that uses a service token with catalog write access. Record what each writer does and which teams depend on it. The step is done when you have a written list with an owner for every writer, so no dependency surprises you after read-only mode is enabled.
Decide the source repository layout
Choose where catalog descriptors live: a dedicated catalog repository, descriptors kept beside each component, or a mix. Define the folder structure, who can approve changes to each area, and how a team registers a new component through a pull request. Agree these rules with platform and security owners, because the layout determines how locations are written in the next step. The step is done when the layout and approval rules are documented and a sample pull request has been reviewed by both teams.
Move locations into version-controlled config
Replace runtime registrations with entries in the Backstage app configuration, typically under the catalog locations section of
app-config.yaml, pointing at the descriptor files in Git. Where entity providers already ingest from a source, confirm which provider covers each location so nothing is ingested twice. Commit the configuration changes through the normal review process. The check is a clean start: after the next processing cycle, every entity you inventoried should appear from a configured location, with its source shown in the entity view.Enable read-only mode
Set
catalog.readonlyto true in the backend configuration and deploy it to a non-production environment first. Then repeat the writer inventory from step one against that environment. Expect the runtime write paths used by plugins and scripts to fail, and record which ones break and why. Decide, for each broken writer, whether to move its output into a descriptor in Git or to retire it. The check is a rejected runtime registration: an attempt to register an entity through the catalog API or UI should return an error rather than create the entity.Set up pull request validation for descriptors
Add a continuous integration job that runs on every pull request touching a
catalog-info.yamlfile. The job should validate the descriptor syntax, required fields such asapiVersion,kindandmetadata.name, and any naming or ownership rules your organization enforces. Make the job a required status check on the default branch so unverified changes cannot merge. The step is done when a deliberately malformed test descriptor fails the check and a valid one passes it.Test that a Git change reaches the catalog
Merge a test descriptor into the source repository and wait for the catalog to process it. Confirm the entity appears with the expected owner, lifecycle and annotations. Then change a field in a second merge, and delete the file in a third, to confirm updates propagate and that removal follows your configured delete or orphan behavior. Note the time the catalog takes to reflect each change so teams know what to expect. The step is done when the create, update and delete cases all behave as documented in your environment.
Confirm the rollout with writers and owners
Repeat the writer inventory in production after the switch and compare it with the list from step one. Share the registration steps with each team, and remove or replace any remaining runtime write paths. Keep a short runbook covering how to request an exception, who approves catalog changes, and how to roll back the read-only setting if a critical plugin breaks. The step is done when no writer remains unaccounted for and the runbook has been used by at least one team to register a component.
What stops working for plugins that write to the catalog
Turning on catalog.readonly changes what the catalog accepts at runtime, so the first job is to list every path that writes today. The obvious ones are UI registration through the catalog import flow, and any script or plugin that calls the catalog API to add, edit or remove entities. Less obvious are custom entity providers that were written on the assumption that they can persist changes, and integrations that register entities as a side effect of a scaffolder run. Search the codebase for calls to the catalog client's write methods, review the backend plugin list for anything that registers locations, and check your scaffolder actions for steps that register components after creating a repository. Confirm the exact behavior of each plugin you run in your own environment, because the set of affected features depends on the plugin versions you have installed.
Once the inventory exists, decide a replacement path for each item. Most registrations should move into a file in a Git repository that the catalog reads through a static location, so the change goes through review and history. Where entities come from a system of record, such as a service inventory or cloud account, a read-only entity provider that pulls from that source is usually the better home, because the source stays authoritative and removals propagate. Scaffolder templates can still create repositories, but the template step should commit a catalog-info.yaml file instead of calling the catalog API directly. Plan the change so teams know where to edit an entity, and test each replacement in a non-production instance before enabling the mode in production.
Security controls that protect a GitOps catalog
In a read-only catalog, Git is the change path, so the security of the catalog is the security of the repositories, their review rules and the credentials that read them. These controls work together with the sign-in resolver, which decides which person a Git change is attributed to inside Backstage.
Branch protection and required reviews
Require pull requests, block direct pushes to the default branch and require at least one approving review before merge on every repository that holds catalog-info.yaml files or static location config. Without this, a single compromised account can add an entity that Backstage then trusts as an owner or documentation source. Check that administrators cannot bypass the rule without leaving an audit record. Confirm the exact settings with your Git provider, since the options differ between GitHub, GitLab and Bitbucket.
CODEOWNERS for ownership changes
Use a CODEOWNERS file so that any change to spec.owner, spec.system or relation fields routes to the platform or catalog team, while changes inside a service's own folder stay with its team. Ownership is what drives notifications, access and cost reporting in Backstage, so an unreviewed owner change quietly moves responsibility. Test the file with a sample pull request from a non-owner before you rely on it.
Restricted edits to static location config
The static locations list in app-config.yaml, which points Backstage at repositories and files, is the most sensitive part of the catalog. Keep it in a repository that only the platform team can merge to, and deploy it through the same pipeline as other Backstage configuration. Anyone who can add a location can pull in arbitrary content, so treat that file like code that runs in production.
Protected Git provider credentials
Backstage reads repositories with a token or GitHub App, GitLab token or similar credential defined in configuration. Grant it read-only repository scope, store it in your secret manager rather than in the config file, and rotate it on a schedule. The integration should not be able to push or change branch protection, so a leaked token cannot alter the catalog it reads.
Sign-in resolver tied to Git identity
Configure sign-in resolvers so every provider maps a verified identity to the same catalog user, as the authentication documentation advises, and disable guest access in production. This matters for the catalog because the person who approved a change in Git should match the user Backstage shows as an owner. Check that a user who signs in through a second provider cannot claim another person's ownership.
Audit trail from Git history
Because every catalog change is a commit, Git history gives you who proposed, reviewed and merged each entity change and when. Keep signed commits or verified merges where your policy requires them, retain the repository history, and protect the audit log from force pushes. Confirm with your auditors which records they will accept as evidence before you design the retention period.
Questions teams ask before running a GitOps catalog
What does it cost in effort to move a Backstage catalog to GitOps?
The main cost is engineering time spent moving entity definitions out of the portal and into repositories, then fixing plugins that expect to write to the catalog. Effort rises with the number of catalog entries that were created through the UI or API, the number of plugins that call the catalog write API, and how many repositories need new ownership and review rules. Effort falls when most entities already live in a Git repository with a descriptor file. Ask any partner for a quote that lists the inventory work, the plugin review, the repository and branch protection setup, and the cutover testing as separate lines, and state which of those are one-time and which recur as the catalog grows.
How long does the transition to a read-only catalog usually take?
The timeline depends on what sets the pace: how many teams must change how they register components, how many plugins depend on catalog writes, and whether your repository owners agree on review rules before cutover. Teams that have a clean inventory of entities and a plugin list usually move faster than teams that discover write paths during the migration. Ask for a plan with explicit gates for inventory, plugin review, a staging cutover and a rollback path, and confirm which of those gates your own change board or security team must approve.
What must our team provide before a GitOps catalog project starts?
Three inputs decide most of the scope. First, a list of the repository owners who will accept catalog descriptors and own their review rules, including who approves changes to shared locations. Second, a plugin inventory that names every installed plugin, which ones write to the catalog, and which ones read only. Third, written approval rules: who may merge a catalog-info.yaml change, what checks run first, and who signs off on new kinds or owners. Without these, the work stalls at design rather than at build.
Can an in-house platform team run this migration without a partner?
Often yes, if the team already operates Backstage, understands its catalog processing and has someone who can review plugin code. The hard parts are less about YAML and more about tracing every write path, handling the entities that were never in Git, and convincing repository owners to adopt the new flow. A partner may add value by running the inventory, testing plugin behavior in a copy of your environment and documenting the cutover. Whichever route you choose, confirm who will own the catalog after launch, because the review and deletion rules must be kept current.
How do teams register a new component in a read-only catalog?
Teams add a catalog-info.yaml file to their repository, or add a path to a location file that the platform team controls, and open a pull request. The change passes validation and review like any other code change, and the catalog picks it up after the next ingestion cycle. Registration requests for shared locations go through the owning team, not the portal UI. Document the steps in your developer guide and confirm how long ingestion takes in your own configuration, since that depends on your processing interval and provider settings.
What happens to catalog entities when their file is deleted from Git?
Entities that a provider or location no longer returns are handled by the orphan and deletion settings in your catalog configuration. Depending on how those settings are set, the entity may be removed or marked as an orphan and kept. Deleting the file in Git is the intended way to retire an entity, but a still-maintained entity can return if another source still describes it. Test the behavior in staging with a deleted file, a moved file and a renamed entity name before you enable the rule in production.
Who can change catalog entries once the catalog is read-only?
Only the process that reads Git changes entries. Users and plugins cannot edit entities through the portal or the catalog API, so ownership changes, lifecycle changes and annotation updates must come through a pull request. Confirm which plugins expected to write to the catalog and decide whether each one is redesigned, switched to a read-only pattern or removed. Access to merge catalog changes should be controlled with branch protection and required reviewers, and that control should be recorded for audit purposes.
Limits to confirm in your environment
A read-only catalog depends on a few behaviors that vary by Backstage release, Git provider and installed plugins. Confirm each one against your own version and configuration before you tell teams that Git is the only path for changes.
Backstage release line and catalog options
Confirm which Backstage release line you run and check the catalog options that release supports for read-only mode and static locations. The main line ships monthly and the next line ships weekly, so a catalog setting that works on one line may behave differently after a bump. Read the release notes for each step, and pin your version so a routine update does not change catalog write behavior without review.
Node.js support for your release line
The versioning policy ties supported Node.js versions to the Backstage release, and only two adjacent even-numbered Node.js releases are supported at a time. Confirm the Node.js version your Backstage release targets before you change your container base image or CI runners. A mismatch can break the catalog backend in ways that look like ingestion failures.
Git provider rate limits and intervals
Read-only catalogs still poll Git providers unless you configure webhooks, so confirm the processing interval and provider schedule against your GitHub, GitLab or Bitbucket API limits. Ask your platform team which service account reads the repositories and how close the polling comes to its quota. Repositories with many descriptor files multiply the calls.
Community plugins that write to the catalog
Some community plugins create or update entities through the catalog API, which a read-only setting blocks. Inventory every installed plugin and check its documentation for write calls, such as registering locations or patching entities. Plugins that fail silently after you turn on read-only mode are easy to miss, so test each one in a staging instance before rollout.
Orphan behavior after config removal
Confirm what the catalog does when you remove a static location or provider from configuration. Depending on your orphan and delete settings, entities may be marked orphaned, kept, or deleted on the next run. Test removal in staging with a sample entity and record the result, because a deleted entity can take its docs, ownership and scorecards with it.
Who can merge catalog changes
Your read-only model is only as strict as the branch protection on the repositories that hold descriptors. Confirm that required reviews, code owners and protected branches apply to the catalog repository, and that service accounts cannot push directly. Ask your security team to verify this with a test merge, not only from the policy document.
How teams register new components without runtime writes
With catalog.readonly enabled, registration becomes a Git workflow. A team adds a catalog-info.yaml to its own repository, or adds a registration entry to a central catalog repository, then opens a pull request against the default branch. Validation runs in CI before merge: the descriptor should be checked against the entity schema, metadata.name should be checked for uniqueness across the catalog, and spec.owner should be checked against a Group that exists in your organization data. Approval from the owning group is enforced through branch protection or code owners, so the person who can merge a registration is someone who answers for that component. Confirm in your version whether the catalog's own validation runs on pull requests or only after merge, because a bad descriptor that reaches the default branch will be ingested on the next refresh.
The central registration repository and per-repository discovery answer ownership and deletion differently. With per-repository discovery, the descriptor lives beside the code it describes, so the owning team controls its own entry and removing the file removes the entity after the provider's next run. A central repository gives the platform team one review point and a single list to audit, but ownership of the entry sits with a different group than ownership of the code, and deleting a registration removes the entity even if the service still runs. Before choosing, confirm with your version's documentation how each provider handles removed files and missing sources, because that behavior decides what happens to an entity whose team has left. Many organizations use per-repository discovery for service metadata and a central repository only for platform-owned resources.
How DeployOpen scopes and delivers a GitOps catalog
A GitOps catalog is mostly a set of decisions about repositories, validation and who may merge changes. DeployOpen fixes those decisions in writing before build work starts, then delivers them inside your own infrastructure with a handover your team can run without us.
Written scope and plugin inventory
The scope starts with an inventory of every plugin, provider and integration that reads from or writes to the catalog. Each item is marked as kept, replaced or removed, because a plugin that writes entities directly will fail once the catalog is read-only. Ask for this inventory in the quote, and confirm which plugins your team depends on today before any work is agreed.
Repository layout and ownership
DeployOpen designs the Git layout that holds catalog-info.yaml files, static location files and any shared descriptors, including which repositories are authoritative and who approves changes to each. The layout decides how teams register components and how a removed file is handled. Confirm the branch protection and code owner rules your Git platform supports before the layout is signed off.
Validation rules in the pipeline
Validation runs in continuous integration before a merge, so malformed descriptors, duplicate names and invalid lifecycle or owner references never reach the catalog. DeployOpen writes the checks, documents each rule and its error message, and agrees the list of rules in the scope. Ask which rules your organization needs to enforce and who can change them later.
Read-only configuration and hardening
The Backstage configuration is set so the catalog accepts changes only from Git, and the implementation covers the configuration keys, access rules and sign-in resolvers that support that setting. Hardening tasks are recorded in the handover so reviewers can test them. Ask for the exact configuration changes and the test evidence that proves writes are refused.
Cutover plan and rollback
The cutover plan sets the order in which existing registrations move to Git, how stale entities are identified, and the point at which write access is removed. A rollback path is written for each step. Ask the team to confirm which services depend on current entities so that the cutover does not orphan them.
Handover, documentation and retainers
Delivery stays inside your cloud accounts, data centers or air-gapped network, and the handover includes runbooks, the repository guide and the operating procedures for adding a component. Optional retainers can cover Backstage upgrades, plugin compatibility and ongoing support after handover. Ask what the retainer covers, how upgrades are scheduled, and who holds administrator access at the end.

