diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md
index e23f628fb6..ea7fe20948 100644
--- a/docs/frontend-system/building-apps/01-index.md
+++ b/docs/frontend-system/building-apps/01-index.md
@@ -74,7 +74,7 @@ Remember that package extensions that are not auto-discovered must be manually a
### Configure extensions individually
-It is possible to enable, disable and configure extensions individually in the `app-config.yaml` config file. To get familiar with what is available for app extensions personalization, go to the [built-in extensions](./02-built-in-extensions.md) documentation. For plugin customizations, we recommend that you read the instructions in each plugin's README file.
+It is possible to enable, disable and configure extensions individually in the `app-config.yaml` config file. To get familiar with what is available for app extensions personalization, go to the [built-in extensions](./03-built-in-extensions.md) documentation. For plugin customizations, we recommend that you read the instructions in each plugin's README file.
### Customize or override built-in extensions
@@ -139,7 +139,7 @@ const app = createApp({
// Calls an async utility method that fetches the config object from the server
const config = await getConfigFromServer();
// Feel free to manipulate the config object before returning it
- // A common example is conditionally modify the config based on the running enviroment
+ // A common example is conditionally modify the config based on the running environment
return { config };
},
});
diff --git a/docs/frontend-system/building-apps/02-configuring-extensions.md b/docs/frontend-system/building-apps/02-configuring-extensions.md
new file mode 100644
index 0000000000..2fd4ce69fc
--- /dev/null
+++ b/docs/frontend-system/building-apps/02-configuring-extensions.md
@@ -0,0 +1,9 @@
+---
+id: configuring-extensions
+title: Configuring Extensions in the App
+sidebar_label: Configuring Extensions
+# prettier-ignore
+description: Documentation for how to configure extensions in a Backstage app
+---
+
+TODO
diff --git a/docs/frontend-system/building-apps/02-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md
similarity index 100%
rename from docs/frontend-system/building-apps/02-built-in-extensions.md
rename to docs/frontend-system/building-apps/03-built-in-extensions.md
diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md
new file mode 100644
index 0000000000..98d8b3bab8
--- /dev/null
+++ b/docs/frontend-system/building-apps/08-migrating.md
@@ -0,0 +1,437 @@
+---
+id: migrating
+title: Migrating Apps
+sidebar_label: Migration Guide
+# prettier-ignore
+description: How to migrate existing apps to the new frontend system
+---
+
+## 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.
+
+## Switching out `createApp`
+
+The first step in migrating an app is to switch out the `createApp` function for the new one from `@backstage/frontend-api-app`:
+
+```tsx title="in packages/app/src/App.tsx"
+// highlight-remove-next-line
+import { createApp } from '@backstage/app-defaults';
+// highlight-add-next-line
+import { createApp } from '@backstage/frontend-app-api';
+```
+
+This immediate switch will lead to a lot of breakages that we need to fix.
+
+Let's start by addressing the change to `app.createRoot(...)`, which 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](../architecture/01-index.md) 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 `convertLegacyApp` and is exported from the `@backstage/core-compat-api` package, which you will need to add as a dependency to your app package:
+
+```bash
+yarn add --cwd packages/app @backstage/core-compat-api
+```
+
+Once installed, import `convertLegacyApp`. If your app currently looks like this:
+
+```tsx title="in packages/app/src/App.tsx"
+const app = createApp({
+ /* other options */
+});
+
+export default app.createRoot(
+ <>
+
+
+
+ {routes}
+
+ >,
+);
+```
+
+Migrate it to the following:
+
+```tsx title="in packages/app/src/App.tsx"
+const legacyFeatures = convertLegacyApp(
+ <>
+
+
+
+ {routes}
+
+ >,
+);
+
+const app = createApp({
+ /* other options */
+ features: [...legacyFeatures],
+});
+
+export default app.createRoot();
+```
+
+We've taken all the elements that were previously passed to `app.createRoot(...)`, and instead passed them to `convertLegacyApp(...)`. We then pass the features returned by `convertLegacyApp` and forward them to the `features` option of the new `createApp`.
+
+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:
+
+```tsx title="in packages/app/src/index.tsx"
+import '@backstage/cli/asset-types';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+// highlight-remove-next-line
+import App from './App';
+// highlight-add-next-line
+import app from './App';
+
+// highlight-remove-next-line
+ReactDOM.createRoot(document.getElementById('root')!).render();
+// highlight-add-next-line
+ReactDOM.createRoot(document.getElementById('root')!).render(app);
+```
+
+At this point the contents of your app should be past the initial migration stage, and we can move on to migrating any remaining options that you may have passed to `createApp`.
+
+## Migrating `createApp` Options
+
+Many of the `createApp` options have been migrated to use extensions instead. Each will have their own [extension creator](../architecture/03-extensions.md#extension-creators) that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to `createExtensionOverrides`, which bundles them into a _feature_ that you can install in the app. See the [standalone extensions](../architecture/05-extension-overrides.md#create-standalone-extensions) 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:
+
+```ts
+const app = createApp({
+ features: [
+ // highlight-add-start
+ createExtensionOverrides({
+ extensions: [lightTheme],
+ }),
+ // highlight-add-end
+ ],
+});
+```
+
+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](../utility-apis/01-index.md) factories are now installed as extensions instead. Pass the existing factory to `createApiExtension` and install it in the app. For more information, see the section on [configuring Utility APIs](../utility-apis/04-configuring.md).
+
+For example, the following API configuration:
+
+```ts
+const app = createApp({
+ apis: [
+ createApiFactory({
+ api: scmIntegrationsApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
+ }),
+ ],
+});
+```
+
+Can be converted to the following extension:
+
+```ts
+const scmIntegrationsApi = createApiExtension({
+ factory: createApiFactory({
+ api: scmIntegrationsApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
+ }),
+});
+```
+
+### `icons`
+
+Icons are currently installed through the usual options to `createApp`, but will be switched to use extensions in the future.
+
+### `plugins`
+
+Plugins are now passed through the `features` options instead.
+
+### `featureFlags`
+
+Declaring features flags in the app is no longer supported, move these declarations to the appropriate plugins instead.
+
+### `components`
+
+Many app components are now installed as extensions instead using `createComponentExtension`. See the section on [configuring app components](./01-index.md#configure-your-app) 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 using the `createSignInPageExtension` instead.
+
+For example, the following sign-in page configuration:
+
+```tsx
+const app = createApp({
+ components: {
+ SignInPage: props => (
+
+ ),
+ },
+});
+```
+
+Can be converted to the following extension:
+
+```tsx
+const signInPage = createSignInPageExtension({
+ loader: async () => props =>
+ (
+
+ ),
+});
+```
+
+### `themes`
+
+Themes are now installed as extensions, using `createThemeExtension`.
+
+For example, the following theme configuration:
+
+```tsx
+const app = createApp({
+ themes: [
+ {
+ id: 'light',
+ title: 'Light',
+ variant: 'light',
+ Provider: ({ children }) => (
+
+ {children}
+
+ ),
+ },
+ ],
+```
+
+Can be converted to the following extension:
+
+```tsx
+const lightTheme = createThemeExtension({
+ id: 'light',
+ title: 'Light Theme',
+ variant: 'light',
+ icon: ,
+ Provider: ({ children }) => (
+
+ ),
+});
+```
+
+### `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.
+
+```ts
+const app = createApp({
+ async configLoader() {
+ const appConfigs = await loadAppConfigs();
+ // highlight-remove-next-line
+ return appConfigs;
+ // highlight-add-next-line
+ return { config: ConfigReader.fromConfigs(appConfigs) };
+ },
+});
+```
+
+### `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](../architecture/07-routes.md#binding-external-route-references) for more information.
+
+Note that if you are binding routes from a legacy plugin that was converted using `convertLegacyApp`, 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:
+
+```ts
+const app = createApp({
+ features: convertLegacyApp(...),
+ bindRoutes({ bind }) {
+ bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), {
+ createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root),
+ });
+ },
+});
+```
+
+### `__experimentalTranslations`
+
+Translations are now installed as extensions, using `createTranslationExtension`.
+
+## Gradual Migration
+
+After updating all `createApp` options as well as using `convertLegacyApp` to use your existing app structure, you should be able to start up the app and see that it still works. If that is not the case, make sure you read any error messages that you may see in the app as they can provide hints on what you need to fix. If you are still stuck, you can check if anyone else ran into the same issue in our [GitHub issues](https://github.com/backstage/backstage/issues), or ask for help in our [community Discord](https://discord.gg/backstage-687207715902193673).
+
+Assuming your app is now working, let's continue by migrating the rest of the app element tree to use the new system.
+
+First off we'll want to trim away any top-level elements in the app so that only the `routes` are left. For example, continuing where we left off with the following elements:
+
+```tsx title="in packages/app/src/App.tsx"
+const legacyFeatures = convertLegacyApp(
+ <>
+
+
+
+ {routes}
+
+ >,
+);
+```
+
+You can remove all surrounding elements and just keep the `routes`:
+
+```tsx title="in packages/app/src/App.tsx"
+const legacyFeatures = convertLegacyApp(routes);
+```
+
+This will remove many extension overrides that `convertLegacyApp` 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](#sidebar), [app root elements](#app-root-elements) and [app root wrappers](#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](#top-level-routes) and [entity pages](#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](../building-plugins/05-migrating.md). 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 `convertLegacyApp(...)` call that you can now remove, and your app should be fully migrated to the new system! 🎉
+
+### Top-level Routes
+
+Your top-level routes are the routes directly under the `AppRouter` component with the `` element. In a small app they might look something like this:
+
+```tsx title="in packages/app/src/App.tsx"
+const routes = (
+
+ } />
+ }
+ >
+ {entityPage}
+
+ } />
+ }
+ />
+
+);
+```
+
+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:
+
+```tsx title="in packages/app/src/App.tsx"
+const routes = (
+
+ } />
+ }
+ >
+ {entityPage}
+
+ {/* highlight-remove-next-line */}
+ } />
+ }
+ />
+
+);
+```
+
+If you are using [app feature discovery](../architecture/02-app.md#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](#entity-pages).
+
+### Entity Pages
+
+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. At the moment we do not provide a way to gradually migrate entity pages to the new system, although that is planned as a future improvement. This means that the entire entity page and all of its plugins need to be migrated at once, including any other usages of those plugins.
+
+### Sidebar
+
+New apps feature a built-in sidebar extension (`app/nav`) that will render all nav item extensions provided by plugins. This is a placeholder implementation and not intended as a long-term solution. In the future we will aim to provide a more flexible sidebar extension that allows for more customization out of the box.
+
+Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an extension override:
+
+```tsx
+const nav = createExtension({
+ namespace: 'app',
+ name: 'nav',
+ attachTo: { id: 'app/layout', input: 'nav' },
+ output: {
+ element: coreExtensionData.reactElement,
+ },
+ factory({ inputs }) {
+ return {
+ element: (
+
+ {/* Sidebar contents from packages/app/src/components/Root.tsx go here */}
+
+ ),
+ };
+ },
+});
+```
+
+### 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:
+
+```tsx
+export default app.createRoot(
+ <>
+ {/* highlight-next-line */}
+
+ {/* highlight-next-line */}
+
+
+ {/* highlight-next-line */}
+
+ {routes}
+
+ >,
+);
+```
+
+The `AlertDisplay` and `OAuthRequestDialog` are already provided as built-in extensions, and so will `VisitListener`. 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:
+
+```tsx
+export default app.createRoot(
+ <>
+
+
+
+ {/* highlight-next-line */}
+
+ {routes}
+ {/* highlight-next-line */}
+
+
+ >,
+);
+```
+
+Any app root wrapper needs to be migrated to be an extension, using `createAppRootWrapperExtension`. 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.
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index 155227f30a..2368ef25f7 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -447,7 +447,9 @@
"label": "Building Apps",
"items": [
"frontend-system/building-apps/index",
- "frontend-system/building-apps/built-in-extensions"
+ "frontend-system/building-apps/configuring-extensions",
+ "frontend-system/building-apps/built-in-extensions",
+ "frontend-system/building-apps/migrating"
]
},
{