Migrating Apps
Overview
This section describes how to migrate an existing Backstage app package to use the new frontend system. The app package is typically found at packages/app
in your project and is responsible for wiring together the Backstage frontend application.
Who is this for?
This guide is intended for maintainers of Backstage app packages (packages/app
) who want to upgrade from the legacy frontend system to the new extension-based architecture.
Prerequisites:
- Familiarity with your app’s current structure and configuration
- Yarn workspaces and monorepo setup
- Access to run
yarn
commands and update dependencies
Migration
We recommend a two-phase migration process to ensure a smooth and manageable transition:
-
Phase 1: Minimal Changes for Hybrid Configuration
In this phase, you make the smallest set of changes necessary to enable your app to run in a hybrid mode. This allows you to start using the new frontend system while still relying on compatibility helpers and legacy code. The goal is to unblock your migration quickly, so you can benefit from the new system without a full rewrite. -
Phase 2: Complete Transition to the New Frontend System
After your app is running in hybrid mode, you can gradually refactor your codebase to remove legacy code and compatibility helpers. This phase focuses on fully adopting the new frontend architecture, ensuring your codebase is clean, maintainable, and takes full advantage of the new features.
Staying in hybrid mode for too long is not recommended. Support for the legacy version and compatibility helpers will be dropped in the future, so we recommend planning to fully migrate your codebase as soon as possible.
Checklist
Before you begin, review this checklist to track your progress:
- Complete minimal changes for hybrid configuration (Phase 1)
- App starts and works in hybrid mode
- Gradually migrate and remove legacy code and helpers (Phase 2)
- App runs fully on the new frontend system
If you encounter issues, check GitHub issues or ask in Discord.
Phase 1: Minimal Changes for Hybrid Configuration
There are 5 steps to minimally change your app to start experimenting with the new frontend system in a hybrid mode.
After completing these steps you should be able to start up the app and see that it still works.
1) Switching out createApp
To start we'll need to add the new @backstage/frontend-defaults
package:
yarn --cwd packages/app add @backstage/frontend-defaults
The next step is to switch out the createApp
function for the new one from @backstage/frontend-defaults
:
import { createApp } from '@backstage/app-defaults';
import { createApp } from '@backstage/frontend-defaults';
This immediate switch will lead to a lot of breakages that will be fixed in the upcoming steps.
2) Converting the createApp
options
Most of the legacy createApp
options — with the exception of bindRoutes
— can be converted into features that are compatible with the new frontend system. This is accomplished using the convertLegacyAppOptions
helper, which allows you to continue using your existing configuration while gradually migrating to the new architecture.
To do so, start by adding this dependency to your app package:
yarn --cwd packages/app add @backstage/core-compat-api
Open the file where your application was created. Currently, you should be doing something like this:
const app = createApp({
apis,
icons: {
// Custom icon example
alert: AlarmIcon,
},
featureFlags: [
{
name: 'scaffolder-next-preview',
description: 'Preview the new Scaffolder Next',
pluginId: '',
},
],
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
);
},
},
});
Migrate it to the following:
import { createApp } from '@backstage/frontend-defaults';
import { convertLegacyAppOptions } from '@backstage/core-compat-api';
const convertedOptionsModule = convertLegacyAppOptions({
/* legacy options such as apis, icons, plugins, components, themes and featureFlags */
apis,
icons: {
// Custom icon example
alert: AlarmIcon,
},
featureFlags: [
{
name: 'scaffolder-next-preview',
description: 'Preview the new Scaffolder Next',
pluginId: '',
},
],
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
);
},
},
}});
const app = createApp({
features: [
// ...
convertedOptionsModule,
],
});
If you were binding routes from a legacy createApp
, you will need to use the convertLegacyRouteRefs
and/or convertLegacyRouteRef
to convert the routes to be compatible with the new system.
For example, if both the catalogPlugin
and scaffolderPlugin
are legacy plugins, you can bind their routes like this:
import { createApp } from '@backstage/frontend-defaults';
import {
// ...
convertLegacyRouteRefs,
convertLegacyRouteRef,
} from '@backstage/core-compat-api';
// Ommitting converted options changes
//...
const app = createApp({
features: [
// ...
convertedOptionsModule,
],
bindRoutes({ bind }) {
bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), {
createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root),
});
},
});
3) Fixing the app.createRoot
call
The app.createRoot(...)
no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to app.createRoot(...)
was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the architecture section.
Given that the app element tree is most of what builds up the app, it's likely also going to be the majority of the migration effort. In order to make the migration as smooth as possible we have provided a helper that lets you convert an existing app element tree into plugins that you can install in a new app. This in turn allows for a gradual migration of individual plugins, rather than needing to migrate the entire app structure at once.
The helper is called convertLegacyAppRoot
and is exported from the @backstage/core-compat-api
package. Once installed, import convertLegacyAppRoot
. If your app currently looks like this:
const app = createApp({
/* All legacy options except route bindings */
});
export default app.createRoot(
<>
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</>,
);
Migrate it to the following:
import {
// ...
convertLegacyAppRoot,
} from '@backstage/core-compat-api';
const convertedRootFeatures = convertLegacyAppRoot(
<>
<AlertDisplay />
<OAuthRequestDialog />
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</>,
);
const app = createApp({
features: [
// ...
...convertedRootFeatures,
],
});
export default app.createRoot();
We've taken all the elements that were previously passed to app.createRoot(...)
, and instead passed them to convertLegacyAppRoot(...)
. We then pass the features returned by convertLegacyAppRoot
and forward them to the features
option of the new createApp
.
4) Adjusting the app rendering
There is one more detail that we need to deal with before moving on. The app.createRoot()
function now returns a React element rather than a component, so we need to update our app index.tsx
as follows:
import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import app from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
ReactDOM.createRoot(document.getElementById('root')!).render(app);
5) Updating the app test file
You'll also need to make similar changes to your App.test.tsx
file as well:
import { render, waitFor } from '@testing-library/react';
import App from './App';
import app from './App';
describe('App', () => {
it('should render', async () => {
process.env = {
NODE_ENV: 'test',
APP_CONFIG: [
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7007' },
},
context: 'test',
},
] as any,
};
const rendered = render(<App />);
const rendered = render(app);
await waitFor(() => {
expect(rendered.baseElement).toBeInTheDocument();
});
});
});
Phase 2: Complete Transition to the New Frontend System
If your app starts and works in hybrid mode, you’re ready to begin Phase 2. If not, review the error messages, check the GitHub issues, or ask for help in our community Discord.
At this point, the contents of your app should have moved past the initial migration stage. Let's continue by gradually removing legacy code and helpers to fully adopt the new system.
Migrating createApp
options
Many of the createApp
options have been migrated to use extensions instead. Each will have their own extension blueprint that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to createFrontendModule
, which bundles them into a feature that you can install in the app. See the frontend module section for more information.
For example, assuming you have a lightTheme
extension that you want to add to your app, you can use the following:
First we add the @backstage/frontend-plugin-api
package
yarn --cwd packages/app add @backstage/frontend-plugin-api
Then we can use it like this:
import { createFrontendModule } from '@backstage/frontend-plugin-api';
const app = createApp({
features: [
// ...
createFrontendModule({
pluginId: 'app',
extensions: [lightTheme],
}),
],
});
You can then also add any additional extensions that you may need to create as part of this migration to the extensions
array as well.
apis
Utility API factories are now installed as extensions instead. Pass the existing factory to ApiBlueprint
and install it in the app. For more information, see the section on configuring Utility APIs.
For example, the following apis
configuration:
const app = createApp({
apis: [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
],
});
Can be converted to the following extension:
import { ApiBlueprint } from '@backstage/frontend-plugin-api';
const scmIntegrationsApi = ApiBlueprint.make({
name: 'scm-integrations',
params: defineParams =>
defineParams({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
});
You would then add scmIntegrationsApi
as an extension
like you did with lightTheme
in the Migrating createApp
Options section.
plugins
Plugins are now passed through the features
options instead.
For example, the following plugins
configuration:
import { homePlugin } from '@backstage/plugin-home';
createApp({
// ...
plugins: [homePlugin],
// ...
});
Can be converted to the following features
configuration:
// plugins are now default exported via alpha subpath
import homePlugin from '@backstage/plugin-home/alpha';
createApp({
// ...
features: [homePlugin],
// ...
});
Plugins don't even have to be imported manually after installing their package if features discovery is enabled.
app:
# Enabling plugin and override features discovery
packages: all # ✨
featureFlags
Declaring features flags in the app is no longer supported, move these declarations to the appropriate plugins instead.
For example, the following app feature flags configuration:
createApp({
// ...
featureFlags: [
{
pluginId: '',
name: 'tech-radar',
description: 'Enables the tech radar plugin',
},
],
// ...
});
Can be converted to the following plugin configuration:
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
createFrontendPlugin({
pluginId: 'tech-radar',
// ...
featureFlags: [{ name: 'tech-radar' }],
// ...
});
This would get added to the features
array as part of your createApp
options.
components
Many app components are now installed as extensions instead using createComponentExtension
. See the section on configuring app components for more information.
The Router
component is now a built-in extension that you can override using createRouterExtension
.
The Sign-in page is now installed as an extension, created using the SignInPageBlueprint
instead.
For example, the following sign-in page configuration:
const app = createApp({
components: {
SignInPage: props => (
<SignInPage
{...props}
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
}}
/>
),
},
});
Can be converted to the following extension:
import { SignInPageBlueprint } from '@backstage/frontend-plugin-api';
const signInPage = SignInPageBlueprint.make({
params: {
loader: async () => props =>
(
<SignInPage
{...props}
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
}}
/>
),
},
});
You would then add signInPage
as an extension
like you did with lightTheme
in the Migrating createApp
Options section.
themes
Themes are now installed as extensions, created using ThemeBlueprint
.
For example, the following theme configuration:
const app = createApp({
themes: [
{
id: 'custom-light',
title: 'Light',
variant: 'light',
Provider: ({ children }) => (
<UnifiedThemeProvider theme={customLightTheme}>
{children}
</UnifiedThemeProvider>
),
},
],
});
Can be converted to the following extension:
import { ThemeBlueprint } from '@backstage/frontend-plugin-api';
const customLightThemeExtension = ThemeBlueprint.make({
name: 'custom-light',
params: {
theme: {
id: 'custom-light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={customLightTheme} children={children} />
),
},
},
});
You would then add customLightThemeExtension
as an extension
like you did with lightTheme
in the Migrating createApp
Options section.
configLoader
The config loader API has been slightly changed. Rather than returning a promise for an array of AppConfig
objects, it should now return the ConfigApi
directly.
import { ConfigReader } from '@backstage/core-app-api';
const app = createApp({
async configLoader() {
const appConfigs = await loadAppConfigs();
return appConfigs;
return { config: ConfigReader.fromConfigs(appConfigs) };
},
});
icons
Icons are now installed as extensions, using the IconBundleBlueprint
to make new instances which can be added to the app.
import { IconBundleBlueprint } from '@backstage/frontend-plugin-api';
const exampleIconBundle = IconBundleBlueprint.make({
name: 'example-bundle',
params: {
icons: {
user: MyOwnUserIcon,
},
},
});
const app = createApp({
features: [
createFrontendModule({
pluginId: 'app',
extensions: [exampleIconBundle],
}),
],
});
bindRoutes
Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on binding routes for more information.
Note that if you are binding routes from a legacy plugin that was converted using convertLegacyAppRoot
, you will need to use the convertLegacyRouteRefs
and/or convertLegacyRouteRef
to convert the routes to be compatible with the new system.
For example, if both the catalogPlugin
and scaffolderPlugin
are legacy plugins, you can bind their routes like this:
const app = createApp({
features: convertLegacyAppRoot(...),
bindRoutes({ bind }) {
bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), {
createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root),
});
},
});
__experimentalTranslations
Translations are now installed as extensions, created using TranslationBlueprint
.
For example, the following translations configuration:
import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha';
createApp({
// ...
__experimentalTranslations: {
resources: [
createTranslationMessages({
ref: catalogTranslationRef,
catalog_page_create_button_title: 'Create Software',
}),
],
},
// ...
});
Can be converted to the following extension:
import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha';
import {
createTranslationMessages,
TranslationBlueprint,
} from '@backstage/frontend-plugin-api';
const catalogTranslations = TranslationBlueprint.make({
name: 'catalog-overrides',
params: {
resource: createTranslationMessages({
ref: catalogTranslationRef,
catalog_page_create_button_title: 'Create Software',
}),
},
});
You would then add catalogTranslations
as an extension
like you did with lightTheme
in the Migrating createApp
Options section.
Migrating createRoot
components
This will remove many extension overrides that convertLegacyAppRoot
put in place, and switch over the shell of the app to the new system. This includes the root layout of the app along with the elements, router, and sidebar. The app will likely not look the same as before, and you'll need to refer to the sidebar, app root elements and app root wrappers sections below for information on how to migrate those.
Once that step is complete the work that remains is to migrate all of the routes and entity pages in the app, including any plugins that do not yet support the new system. For information on how to migrate your own internal plugins, refer to the plugin migration guide. For external plugins you will need to check the migration status of each plugin and potentially contribute to the effort.
Once these migrations are complete you should be left with an empty convertLegacyAppRoot(...)
call that you can now remove, and your app should be fully migrated to the new system! 🎉
App Root Elements
App root elements are React elements that are rendered adjacent to your current Root
component. For example, in this snippet AlertDisplay
, OAuthRequestDialog
and VisitListener
are all app root elements:
const convertedRootFeatures = convertLegacyAppRoot(
<>
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<AppRouter>
<VisitListener />
<Root>{routes}</Root>
</AppRouter>
</>,
);
The AlertDisplay
and OAuthRequestDialog
are already provided as built-in extensions, and so will VisitListener
, so you can remove all surrounding elements and just keep the routes
:
const convertedRootFeatures = convertLegacyAppRoot(routes);
But, if you have your own custom root elements you will need to migrate them to be extensions that you install in the app instead. Use createAppRootElementExtension
to create said extension and then install it in the app.
Whether the element used to be rendered as a child of the AppRouter
or not doesn't matter. All new root app elements will be rendered as a child of the app router.
App Root Wrappers
App root wrappers are React elements that are rendered as a parent of the current Root
elements. For example, in this snippet the CustomAppBarrier
is an app root wrapper:
const convertedRootFeatures = convertLegacyAppRoot(
<>
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<AppRouter>
<CustomAppBarrier>
<Root>{routes}</Root>
</CustomAppBarrier>
</AppRouter>
</>,
);
Any app root wrapper needs to be migrated to be an extension, created using AppRootWrapperBlueprint
. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper.
Here is an example converting the CustomAppBarrier
into extension:
createApp({
// ...
features: [
createFrontendModule({
pluginId: 'app',
extensions: [
AppRootWrapperBlueprint.make({
name: 'custom-app-barrier',
params: {
// Whenever your component uses legacy core packages, wrap it with "compatWrapper"
// e.g. props => compatWrapper(<CustomAppBarrier {...props} />)
Component: CustomAppBarrier,
},
}),
],
}),
],
// ...
});
App Root Sidebar
New apps feature a built-in sidebar extension which is created by using the NavContentBlueprint
in src/modules/nav/Sidebar.tsx
. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items which are the other NavItem
extensions provided by the system.
In order to migrate your existing sidebar, you will want to create an override for the app/nav
extension. You can do this by copying the standard of having a src/modules/nav/
folder, which can contain an extension which you can install into the app
in the form of a module
.
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SidebarContent } from './Sidebar';
export const navModule = createFrontendModule({
pluginId: 'app',
extensions: [SidebarContent],
});
Then in the actual implementation for the SidebarContent
extension, you can provide something like the following, where the component that is passed to the compatWrapper
is the entire Sidebar
component from your Root
component.
The compatWrapper
is there to ensure that any legacy plugins using things like useRouteRef
work well in the new system, so if you run into some errors which look like compatibility issues, make sure that this wrapper is used in the relevant places.
import { compatWrapper } from '@backstage/core-compat-api';
import { NavContentBlueprint } from '@backstage/frontend-plugin-api';
export const SidebarContent = NavContentBlueprint.make({
params: {
component: ({ items }) =>
compatWrapper(
<Sidebar>
<SidebarLogo />
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
<SidebarSearchModal />
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
...
</SidebarGroup>
<SidebarGroup label="Plugins">
<SidebarScrollWrapper>
{/* Items in this group will be scrollable if they run out of space */}
{items.map((item, index) => (
<SidebarItem {...item} key={index} />
))}
</SidebarScrollWrapper>
</SidebarGroup>
</Sidebar>,
),
},
});
The items
property is a list of all extensions provided by the NavItemBlueprint
that are currently installed in the App. If you don't want to auto populate this list you can simply remove the rendering of that SidebarGroup
, but otherwise you can see from the above example how a SidebarItem
element is rendered for each of the items in the list.
You might also notice that when you're rendering additional fixed icons for plugins that these might become duplicated as the plugin provides a NavItem
extension and you're also rendering one in the Sidebar
manually. In order to remove the item from the list of items
which is passed through, we recommend that you disable that extension using config:
app:
extensions:
- nav-item:search: false
- nav-item:catalog: false
You can also determine the order of the provided auto installed NavItems
that you get from the system in config. The below example ensures that the catalog
navigation item will proceed the search
navigation item when being passed through as the item
prop.
app:
extensions:
- nav-item:catalog
- nav-item:search
App Root Routes
Your top-level routes are the routes directly under the AppRouter
component with the <FlatRoutes>
element. In a small app they might look something like this:
const routes = (
<FlatRoutes>
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
<Route path="/create" element={<ScaffolderPage />} />
<Route
path="/tech-radar"
element={<TechRadarPage width={1500} height={800} />}
/>
</FlatRoutes>
);
Each of these routes needs to be migrated to the new system. You can do it as gradually as you want, with the only restriction being that all routes from a single plugin must be migrated at once. This is because plugins discovered from these legacy routes will override any plugins that are installed in your app. If you for example only migrate one of the two routes defined by a plugin, the other route will remain and still override any plugin with the same ID, and you're left with a partial and likely broken plugin.
To migrate a route, you need to remove it from your list of routes and instead install the new version of the plugin in your app. Before doing this you should make sure that the plugin supports the new system. Let's remove the scaffolder route as an example:
const routes = (
<FlatRoutes>
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
<Route path="/create" element={<ScaffolderPage />} />
<Route
path="/tech-radar"
element={<TechRadarPage width={1500} height={800} />}
/>
</FlatRoutes>
);
If you are using app feature discovery the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually install the new scaffolder plugin in your app through the features
option of createApp
.
Continue this process for each of your legacy routes until you have migrated all of them. For any plugin with additional extensions installed as children of the Route
, refer to the plugin READMEs for more detailed instructions. For the entity pages, refer to the separate section.
Migrating core, internal and third-party plugins
For certain core plugins — such as the Catalog plugin's entity page — we provide a dedicated step-by-step migration guide, since these plugins often require a more gradual approach due to their complexity.
Refer to the plugin migration guide for instructions on migrating internal plugins. For external plugins, check their migration status and contribute if needed.
Catalog Entity Page
The entity pages are typically defined in packages/app/src/components/catalog
and rendered as a child of the /catalog/:namespace/:kind/:name
route. The entity pages are typically quite large and bringing in content from quite a lot of different plugins. To help gradually migrate entity pages we provide the entityPage
option in the convertLegacyAppRoot
helper. This option lets you pass in an entity page app element tree that will be converted to extensions that are added to the features returned from convertLegacyAppRoot
.
To start the gradual migration of entity pages, add your entityPages
to the convertLegacyAppRoot
call:
const convertedRootFeatures = convertLegacyAppRoot(routes);
const convertedRootFeatures = convertLegacyAppRoot(routes, { entityPage });
Next, you will need to fully migrate the catalog plugin itself. This is because only a single version of a plugin can be installed in the app at a time, so in order to start using the new version of the catalog plugin you need to remove all usage of the old one. This includes both the routes and entity pages. You will need to keep the structural helpers for the entity pages, such as EntityLayout
and EntitySwitch
, but remove any extensions like the <CatalogIndexPage/>
and entity cards and content like <EntityAboutCard/>
and <EntityOrphanWarning/>
.
Remove the following routes:
const routes = (
<FlatRoutes>
...
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
...
</FlatRoutes>
);
And explicitly install the catalog plugin before the converted legacy features:
import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha';
const app = createApp({
features: [convertedOptionsModule, ...convertedRootFeatures],
features: [catalogPlugin, convertedOptionsModule, ...convertedRootFeatures],
});
If you are not using the default <CatalogIndexPage />
you can install your custom catalog page as an override for now instead, and fully migrate it to the new system later.
const catalogPluginOverride = catalogPlugin.withOverrides({
extensions: [
catalogPlugin.getExtension('page:catalog').override({
params: {
loader: async () => (
<CatalogIndexPage
pagination={{ mode: 'offset', limit: 20 }}
filters={<>{/* ... */}</>}
/>
),
},
}),
],
});
const app = createApp({
features: [
catalogPlugin,
catalogPluginOverride,
convertedOptionsModule,
...convertedRootFeatures,
],
});
At this point you should be able to run the app and see that you're not using the new version of the catalog plugin. If you navigate to the entity pages you will likely see a lot of duplicate content at the bottom of the page. These are the duplicates of the entity cards provided by the catalog plugin itself that we mentioned earlier that you need to remove. Clean up the entity pages by removing cards and content from the catalog plugin such as <EntityAboutCard/>
and <EntityOrphanWarning/>
.
Once the cleanup is complete you should be left with clean entity pages that are built using a mix of the old and new frontend system. From this point you can continue to gradually migrate plugins that provide content for the entity pages, until all plugins have been fully moved to the new system and the entityPage
option can be removed.
Migrating across the tabs for the Entity Pages should be as simple as removing the EntityLayout.Route
for each of the plugins that provide tab content, and then this tab should be sourced from the EntityContent
extensions created by the plugins themselves which will be automatically detected and added to the App.
Enable the new templates for yarn new
It's encouraged that once you switch over to using the new frontend system, that new plugins that you create are using the new frontend system. This means that you're not instantly creating legacy plugins that will eventually need migration.
This practice is also pretty important early on, as it's going to help you get familiar with the practices of the new frontend system.
When creating a new Backstage app with create-app
and using the --next
flag you'll automatically get these choices in the yarn new
command, but if you want to bring these templates to an older app, you can add the following to your root package.json
:
{
...
"scripts": {
...
"new": "backstage-cli new"
},
"backstage": {
"cli": {
"new": {
"globals": {
"license": "UNLICENSED"
},
"templates": [
"@backstage/cli/templates/new-frontend-plugin",
"@backstage/cli/templates/new-frontend-plugin-module",
"@backstage/cli/templates/backend-plugin",
"@backstage/cli/templates/backend-plugin-module",
"@backstage/cli/templates/plugin-web-library",
"@backstage/cli/templates/plugin-node-library",
"@backstage/cli/templates/plugin-common-library",
"@backstage/cli/templates/web-library",
"@backstage/cli/templates/node-library",
"@backstage/cli/templates/scaffolder-backend-module"
]
}
}
}
}
Troubleshooting
We'd recommend that you install the app-visualizer
plugin to help your troubleshooting. If you run yarn add @backstage/plugin-app-visualizer
in packages/app
it should be automatically added to the sidebar, and available on /visualizer
.
There is a tree
mode that can be very helpful in understanding which plugins are being automatically detected and the extensions that they are providing to the system. You should also be able to see any legacy extensions which are being converted and added to the app.
This can be really useful output when raising any issue to the main repository too, so we can dig in to see what's happening with the system.
I'm seeing duplicate cards for Entity Pages
When using the entityPage
option with convertLegacyAppRoot
, you may notice duplicate cards appearing on your Entity Pages. This happens because the migration helper automatically extracts cards from your existing Entity Page component and adds them to the new system, while the new Entity Page system also automatically includes cards from any plugins installed in your packages/app
package. This results in the same card appearing twice - once from your legacy component and once from the plugin.
To fix this, simply remove the card definitions from your old Entity Page component. The new system will automatically provide these cards through the installed plugins, so your manual definitions are no longer needed.
Error: Invalid element inside FlatRoutes, expected Route but found element of type ...
This means that the Routes
inside FlatRoutes
contains something other than a Route
element. This could be for example a FeatureFlag
or RequirePermissions
element. These are not currently supported by the new frontend system. Workarounds include pushing this logic down from the App.tsx
routes into the plugins themselves as these elements no longer need to live in the App.tsx
for the system to be able to walk and collect the plugins and routes that are available in the App.
If you have a use case where these are required, please reach out to us either through a bug report or the community Discord.
Next Steps
- See architecture docs for more on the new system.
- If you encounter issues, check GitHub issues or ask in Discord.