diff --git a/.changeset/permission-api-batching.md b/.changeset/permission-api-batching.md
new file mode 100644
index 0000000000..857422665d
--- /dev/null
+++ b/.changeset/permission-api-batching.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-permission-react': patch
+---
+
+The `PermissionApi.authorize()` method now accepts arrays of permission requests, and permission checks made in the same tick are batched into a single call to the permission backend.
diff --git a/.changeset/plugin-app-bootstrap-phase.md b/.changeset/plugin-app-bootstrap-phase.md
new file mode 100644
index 0000000000..c3bab9e563
--- /dev/null
+++ b/.changeset/plugin-app-bootstrap-phase.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-app': patch
+---
+
+Updated the default app root to better support phased app preparation by allowing the app layout to be absent during bootstrap, routing bootstrap failures through the app root boundary, and avoiding installation of a guest identity in protected apps that do not provide a sign-in page.
diff --git a/docs/frontend-system/architecture/10-app.md b/docs/frontend-system/architecture/10-app.md
index 22457bcb30..bfc667f664 100644
--- a/docs/frontend-system/architecture/10-app.md
+++ b/docs/frontend-system/architecture/10-app.md
@@ -40,6 +40,36 @@ Each node in this tree is an extension with a parent node and children. The colo
A common type of data that is shared between extensions is React elements and components. These can in turn be rendered by each other in their own React components, which ends up forming a parallel tree of React components that is similar in shape to that of the app extension tree. At the top of the app extension tree is a built-in root extension that among other things outputs a React element. This element also ends up being the root of the parallel React tree, and is rendered by the React element returned by `app.createRoot()`.
+## Preparing an App in Phases
+
+Most apps should use `createApp` from `@backstage/frontend-defaults`, which takes care of all app preparation internally. For more advanced use cases there is also a lower-level `prepareSpecializedApp` API in `@backstage/frontend-app-api`.
+
+This API is useful when you need to render a bootstrap tree before the full app can be finalized, for example while waiting for sign-in or other session-dependent state. It gives you access to a bootstrap app tree immediately, notifies you when the finalized app is ready, and lets you reuse a prepared session in a later app instance.
+
+```tsx
+import {
+ FinalizedSpecializedApp,
+ prepareSpecializedApp,
+} from '@backstage/frontend-app-api';
+
+const preparedApp = prepareSpecializedApp({
+ config,
+ features: [appPlugin, ...features],
+});
+
+const bootstrapApp = preparedApp.getBootstrapApp();
+
+const unsubscribe = preparedApp.onFinalized(
+ (finalizedApp: FinalizedSpecializedApp) => {
+ console.log(finalizedApp.sessionState);
+ },
+);
+```
+
+The `getBootstrapApp()` method exposes the partial app tree that is available during bootstrap. The `onFinalized()` method notifies you once the full app tree has been finalized, and `finalize(sessionState?)` can be used when you already have a reusable session state or when you want to bypass the asynchronous bootstrap flow in tests.
+
+When using phased app preparation, `app/root.children` acts as the main session boundary. Conditional extensions behind that boundary are evaluated during finalization. Conditional `app/root.elements` and API branches are also deferred until finalization, while other bootstrap-visible predicates are ignored and reported as warnings.
+
## Feature Discovery
App feature discovery lets you automatically discover and install features provided by dependencies in your app. In practice, it means that you don't need to manually `import` features in code, but they are instead installed as soon as you add them as a dependency in your `package.json`.
diff --git a/docs/frontend-system/architecture/15-plugins.md b/docs/frontend-system/architecture/15-plugins.md
index c2f5f237c3..d124a0e61d 100644
--- a/docs/frontend-system/architecture/15-plugins.md
+++ b/docs/frontend-system/architecture/15-plugins.md
@@ -76,6 +76,20 @@ These are the routes that the plugin exposes to the app. The `routes` option dec
This is a list of feature flag declarations that your plugin provides to the app. This makes sure that the feature flags are correctly registered and can be toggled in the app. To read a feature flag you can use the feature flags [Utility API](../architecture/33-utility-apis.md), accessible via `featureFlagsApiRef`.
+### `if` option
+
+The `if` option lets you apply a shared condition to all extensions that are provided by a plugin instance. This is useful when you want to gate an entire plugin behind a feature flag or permission without repeating the same predicate on every individual extension.
+
+```tsx
+export default createFrontendPlugin({
+ pluginId: 'my-plugin',
+ if: { featureFlags: { $contains: 'my-plugin-enabled' } },
+ extensions: [...],
+});
+```
+
+This predicate is applied to every extension from that plugin instance. If any extension already has its own `if` predicate, the two are combined using logical `AND`.
+
### `info` option
This options is used to provide loaders for different sources of information about the plugin that may be useful to users and admins. The two available loaders are `packageJson` and `manifest`, and a plugin can use either or both as needed. The resulting information is available via the `info()` method on the plugin instance once it is installed in an app, but it is up to each app to decide how to derive the information from the provided sources.
diff --git a/docs/frontend-system/architecture/20-extensions.md b/docs/frontend-system/architecture/20-extensions.md
index 106ab5340a..3df33b9cad 100644
--- a/docs/frontend-system/architecture/20-extensions.md
+++ b/docs/frontend-system/architecture/20-extensions.md
@@ -41,6 +41,43 @@ Each extension in the app can be disabled, meaning it will not be instantiated a
The ordering of extensions is sometimes very important, as it may for example affect in which order they show up in the UI. When an extension is toggled from disabled to enabled through configuration it resets the ordering of the extension, pushing it to the end of the list. It is generally recommended to leave extensions as disabled by default if their order is important, allowing for the order in which their are enabled in the configuration to determine their order in the app.
+### Conditions
+
+Extensions can also be conditionally enabled by providing an `if` predicate. This is available both on `createExtension(...)` directly and when creating extensions from blueprints.
+
+The predicate uses the same `FilterPredicate` syntax as elsewhere in Backstage, but in the frontend system it is evaluated against app-level data such as `featureFlags` and `permissions`. For example, the following page is only installed when the `experimental-features` flag is active:
+
+```tsx
+const examplePage = PageBlueprint.make({
+ params: {
+ path: '/example',
+ loader: () => import('./ExamplePage').then(m => ),
+ },
+ if: { featureFlags: { $contains: 'experimental-features' } },
+});
+```
+
+You can also combine conditions using logical operators such as `$all`, `$any`, and `$not`:
+
+```tsx
+const guardedCard = CardBlueprint.make({
+ params: {
+ title: 'Guarded Card',
+ loader: () => import('./GuardedCard').then(m => ),
+ },
+ if: {
+ $all: [
+ { featureFlags: { $contains: 'experimental-features' } },
+ { permissions: { $contains: 'catalog.entity.create' } },
+ ],
+ },
+});
+```
+
+Conditions are evaluated when the app tree is prepared, not continuously while the app is running. If the underlying feature flags or permissions change, the app needs to be prepared again in order for the extension tree to change, which in practice typically means reloading the app.
+
+If a plugin or module also provides an `if` predicate, it is combined with the extension-level predicate using logical `AND`. See the [plugin `if` option](./15-plugins.md#if-option) and [frontend modules](./25-extension-overrides.md#creating-a-frontend-module) sections for more details.
+
### Configuration & configuration schema
Each extension can define a configuration schema that describes the configuration that it accepts. This schema is used to validate the configuration provided by integrators, but also to fill in default configuration values. The configuration itself is provided by integrators in order to customize the extension. It is not possible to provide a default configuration of an extension, this must instead be done through defaults in the configuration schema. This allows for a simpler configuration logic where multiple configurations of the same extension completely replace each other rather than being merged.
diff --git a/docs/frontend-system/architecture/23-extension-blueprints.md b/docs/frontend-system/architecture/23-extension-blueprints.md
index f6efccccbb..97eb01022d 100644
--- a/docs/frontend-system/architecture/23-extension-blueprints.md
+++ b/docs/frontend-system/architecture/23-extension-blueprints.md
@@ -9,7 +9,7 @@ The `createExtension` function and related APIs is considered a low-level buildi
## Creating an extension from a blueprint
-Every extension blueprint provides a `make` method that can be used to create new extensions. It is a simple way to create a new extension where the base blueprint provides all the necessary functionality. All you need to do is to provide the necessary blueprint parameters, but you also have the ability to provide additional options, for example a `name` for the extension.
+Every extension blueprint provides a `make` method that can be used to create new extensions. It is a simple way to create a new extension where the base blueprint provides all the necessary functionality. All you need to do is to provide the necessary blueprint parameters, but you also have the ability to provide additional options, for example a `name`, `attachTo`, `disabled`, or `if` predicate for the extension.
The following is a simple example of how one might use the blueprint `make` method to create a new extension:
diff --git a/docs/frontend-system/architecture/25-extension-overrides.md b/docs/frontend-system/architecture/25-extension-overrides.md
index 5e684476c8..b3a7759867 100644
--- a/docs/frontend-system/architecture/25-extension-overrides.md
+++ b/docs/frontend-system/architecture/25-extension-overrides.md
@@ -343,3 +343,5 @@ export default app.createRoot();
```
You must define a `pluginId` when creating a frontend module, and the plugin must also be installed for the module to be loaded.
+
+Frontend modules also support an `if` option. Just like for plugins, that predicate is applied to every extension that comes from the module, and is combined with any extension-level `if` predicate using logical `AND`. This is useful when you want to enable or disable an entire override package based on a feature flag or permission.
diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md
index bd9a0fbae5..cfef547afc 100644
--- a/docs/frontend-system/building-apps/01-index.md
+++ b/docs/frontend-system/building-apps/01-index.md
@@ -57,6 +57,8 @@ Note that `createRoot` returns the root element that is rendered by React. The a
Visit the [built-in extensions](#customize-or-override-built-in-extensions) section to see what is installed by default in a Backstage application.
+For advanced bootstrap flows that need access to the app tree before the full app is finalized, see [preparing an app in phases](../architecture/10-app.md#preparing-an-app-in-phases).
+
## Configure your app
### Bind external routes
diff --git a/docs/frontend-system/building-apps/03-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md
index e1d0b0282d..05e60b54b7 100644
--- a/docs/frontend-system/building-apps/03-built-in-extensions.md
+++ b/docs/frontend-system/building-apps/03-built-in-extensions.md
@@ -99,7 +99,7 @@ This is the extension that creates the app root element, so it renders root leve
| ---------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| router | A React component that should manager the app routes context. | It must be one [router](https://reactrouter.com/en/main/routers/picking-a-router#web-projects) component or a custom component compatible with the 'react-router' library. | true | [BrowserRouter](https://reactrouter.com/en/main/router-components/browser-router) | [createRouterExtension](https://backstage.io/docs/reference/frontend-plugin-api.createrouterextension) |
| signInPage | A React component that should render the app sign-in page. | Should call the `onSignInSuccess` prop when the user has been successfully authorized, otherwise the user will not be correctly redirected to the application home page. | true | The default `AppRoot` extension does not use a default component for this input, it bypasses the user authentication check and always renders all routes when a login page is not installed. | [createSignInPageExtension](https://backstage.io/docs/reference/frontend-plugin-api.createsigninpageextension/) |
-| children | A React component that renders the app sidebar and main content in a particular layout. | - | false | The [`App/Layout`](#app-layout) extension output. | No creator available, configure or override the [`App/Layout`](#app-layout) extension. |
+| children | A React component that renders the app sidebar and main content in a particular layout. | - | true | The [`App/Layout`](#app-layout) extension output. | No creator available, configure or override the [`App/Layout`](#app-layout) extension. |
| elements | React elements to be rendered outside of the app layout, such as shared popups. | - | false | See [default elements](#default-app-root-elements-extensions). | [createAppRootElementExtension](https://backstage.io/docs/reference/frontend-plugin-api.createapprootelementextension/) |
| wrappers | React components that should wrap the root element. | - | true | - | [createAppRootWrapperExtension](https://backstage.io/docs/reference/frontend-plugin-api.createapprootwrapperextension/) |