This guide introduces you to the SCION Microfrontend Platform and covers all the concepts and API that are integral to the SCION Microfrontend Platform. Terms and expressions used in this guide are listed in chapter Terminology. This guide assumes that you are already familiar with HTML and TypeScript.

SCION Microfrontend Platform
SCION Microfrontend Platform API Reference

Refer to the API Reference for full details of the SCION Microfrontend Platform API.

Explore SCION Microfrontend Platform

Play around with our technical demo application to explore the platform and experiment with its features.

Feedback and Contributions

We encourage other developers to join the project and contribute to making SCION Microfrontend Platform constantly better and more stable. If you are missing a feature, please create a feature request so we can discuss it and coordinate further development. To report a bug, please check existing issues first, and if found, leave a comment on the issue. Otherwise, file a bug or, even better, create a pull request with a proposed fix.

For more details, see our CONTRIBUTING GUIDELINES.

SCION Microfrontend Platform

SCION Microfrontend Platform is a TypeScript-based open source library that enables the implementation of a framework-agnostic microfrontend architecture using iframes. It provides fundamental APIs for microfrontends to communicate with each other across origins and facilitates embedding microfrontends using a web component and a router. SCION Microfrontend Platform is a lightweight, web stack agnostic library that has no user-facing components and does not dictate any form of application structure.

You can continue using the frameworks you love since the platform integrates microfrontends via iframes. Iframes by nature provide maximum isolation and allow the integration of any web application without complex adaptation. The platform aims to shield developers from iframe specifics and the low-level messaging mechanism to focus instead on integrating microfrontends.

Cross-microfrontend communication

The platform adds a pub/sub layer on top of the native postMessage mechanism to enable microfrontends to communicate with each other easily across origins. Communication comes in two flavors: topic-based and intent-based. Both models feature request-response message exchange, support retained messages for late subscribers to receive the latest messages, and provide API to intercept messages to implement cross-cutting messaging concerns.

Topic-based messaging enables you to publish messages to multiple subscribers via a common topic. Intent-based communication focuses on controlled collaboration between applications. To collaborate, an application must express an intention. Manifesting intentions enables us to see dependencies between applications down to the functional level.

See chapters Cross Application Communication and Intention API for more information.

Microfrontend Integration and Routing

The platform makes it easy to integrate microfrontends through its router-outlet. The router-outlet is a web component that wraps an iframe. It solves many of the cumbersome quirks of iframes and helps to overcome iframe restrictions. For example, it can adapt its size to the preferred size of embedded content, supports keyboard event propagation and lets you pass contextual data to embedded content. Using the router, you control which web content to display in an outlet. Multiple outlets can display different content, determined by different outlet names, all at the same time. Routing works across application boundaries and enables features such as persistent navigation.

See chapter Embedding Microfrontends for more information.


A microfrontend architecture can be achieved in many ways, each with its pros and cons. The SCION Microfrontend Platform uses the iframe approach primarily since iframes by nature provide the highest possible level of isolation through a separate browsing context. The SCION Microfrontend Platform provides you with the necessary tools to best support you in implementing such an architecture.

1. Microfrontend Architecture

Web frontends are becoming more and more common, even for complex business applications. To tackle the complexity of enterprise application landscapes, a strong trend towards microservice-based backends and microfrontends on the client-side is emerging. The microfrontend design approach is very tempting and has obvious advantages, especially for large-scale and long-lasting projects, most notably because we are observing an enormous dynamic in web frameworks.

microfrontend architecture

The microservice and microfrontend architecture design approach enables us to form development teams full-stack in line with the business functionality, resulting in independent so-called micro applications. A micro application deals with well-defined business functionality. Its backend services are collectively referred to as microservice and its user-facing parts as microfrontends.

A microfrontend is a term of the microfrontend architecture design approach to developing frontend applications as a composition of small, self-contained components, so-called microfrontends. Each microfrontend focuses on a single business functionality, breaking up hard-to-handle monoliths into parts by allowing independent development, autonomous lifecycles, true code splitting, and the use of different stacks. Microfrontends should be as independent and isolated as possible so that a change in one microfrontend has no impact on other microfrontends.

For the user, however, it is still a single application. The composition of the microfrontends is transparent. A consistent look and feel across microfrontends further contributes to a coherent user experience.

2. Technology and Limitations

SCION Microfrontend Platform uses the iframe approach for embedding microfrontends. An iframe provides the highest possible level of isolation through a separate browsing context, allowing the integration of any web page and the use of different web stacks. With iframes, however, there also come restrictions. If embedding a web application in an iframe, the browser loads the app from scratch, so embedded web applications must be optimized for small bundle size and fast startup time. While the separate browsing context is excellent in terms of application isolation, it makes it more difficult to maintain state across same-application instances and to communicate across application boundaries. Also, web content embedded in an iframe is clipped at the iframe boundaries, preventing overlays from overlapping it.

The platform provides API to shield developers from the iframe element and the low-level postMessage mechanism to focus instead on integrating microfrontends. It adds a pub/sub layer on top of the postMessage mechanism and provides a web component for embedding microfrontends using a router. SCION Microfrontend Platform is written in TypeScript and has a peer dependency on RxJS.

3. Core Concepts

This part of the developer guide covers all the concepts and API that are integral to the SCION Microfrontend Platform. Refer to chapter Terminology for an overview of the terms and expressions used in this guide.

3.1. Overview

SCION Microfrontend Platform provides you the fundamental building blocks for implementing a microfrontend architecture using iframes. It provides APIs for microfrontends to communicate with each other across origin, allows embedding microfrontends using a web component and enables routing between microfrontends. SCION Microfrontend Platform is a lightweight, web stack agnostic library that has no user-facing components and does not dictate any form of application structure.

This chapter explains the core concepts of the platform and introduces you to important APIs.

For illustration purposes, we use a simple webshop in this chapter to explain the core concepts of the SCION Microfrontend Platform. The user can choose a product from the product catalog, show customer reviews for a product, add products to the shopping cart, and finally checkout. The webshop consists of multiple web applications that contribute microfrontends.

Microfrontend

A microfrontend is a term of the microfrontend architecture design approach to developing frontend applications as a composition of small, self-contained components, so-called microfrontends. Each microfrontend focuses on a single business functionality, breaking up hard-to-handle monoliths into parts by allowing independent development, autonomous lifecycles, true code splitting, and the use of different stacks. Microfrontends should be as independent and isolated as possible so that a change in one microfrontend has no impact on other microfrontends.

The following figure shows the webshop that consists of multiple microfrontends provided by different web applications. The color of a microfrontend indicates the web app which provides the microfrontend.

webshop microfrontends

Microfrontends can be nested, as illustrated by the product microfrontend that embeds the customer-reviews microfrontend. A single web application can provide multiple microfrontends, as shown by the Product Catalog Application that provides the product-list and product microfrontends.

SCION Microfrontend Platform uses iframes to embed microfrontends; thus, any web page can be integrated as a microfrontend. If the providing web app is registered with the platform, the microfrontend can further interact with other microfrontends. Registered web apps are referred to as micro applications.

Micro Application

A micro application deals with well-defined business functionality. It is a regular web application that provides one or more microfrontends. A micro application can communicate with other micro applications safely using the platform's cross-origin messaging API. A micro application has to provide an application manifest which to register in the host application.

A micro application provides zero, one or more microfrontends and may contribute functionality to other micro applications via the Intention API. Micro applications can communicate with each other via client-side messaging.

The following figure shows the micro applications of the webshop application and the microfrontends they provide.

webshop micro applications

Every micro application must provide an application manifest. The manifest is a JSON file that contains information about the micro application. If using the Intention API, the micro application declares its intentions and capabilities in the manifest file.

When a microfrontend is displayed, the microfrontend starts a new instance of its micro application, resulting in zero, one or more instances of the same micro application running simultaneously. Each microfrontend instance forms a separate browsing context with a separate Window and Document. Consequently, sharing state client-side between the different instances of the same micro application is limited to using session or local storage, broadcast channel, or client-side messaging.

Host Application

The host application, sometimes also called the container application, provides the top-level integration container for microfrontends. Typically, it is the web app which the user loads into the browser that provides the main application shell, defining areas to embed microfrontends.

The host application is the topmost application for integrating microfrontends. It starts the platform host and registers the micro applications.

It is conceivable - although rare - to have more than one host app in the application. As a prerequisite, every host app must be unique in its window hierarchy, i.e., must not be embedded (nor directly nor indirectly) by another host app. Each host app forms a separate and completely isolated namespace.

3.2. Cross Application Communication

This part explains how micro applications and microfrontends can communicate with each other.

Cross-application communication is an integral part when implementing a microfrontend architecture. By using the browser’s native postMessage() mechanism, you can send messages to applications loaded from different domains. For posting a message, however, you need a reference to the Window of the receiving application, which can quickly become complicated, also due to restrictions imposed by the Same-origin Policy.

The SCION Microfrontend Platform provides a client-side Messaging API on top of the native postMessage mechanism to enable microfrontends to communicate with each other easily across origins. The Messaging API offers publish/subscribe messaging to microfrontends in two flavors: Topic-Based Messaging and Intent-Based Messaging.

Data sent from one JavaScript realm to another is serialized with the Structured Clone Algorithm. The algorithm supports structured objects such as nested objects, arrays, and maps.
Cross-application communication should be reduced to a minimum to keep the applications as independent as possible. Also, be careful not to introduce tight data coupling between applications, as this would mitigate the advantage of microfrontends.

3.2.1. Topic-Based Messaging

This chapter introduces the topic-based communication for exchanging data and sending events between micro applications.


What is Topic-Based Messaging?

Topic-based messaging allows publishing a message to a topic, which then is transported to consumers subscribed to the topic. Topics are case-sensitive and consist of one or more segments, each separated by a forward slash. Consumers can subscribe to multiple topics simultaneously by using wildcard segments in the topic.

The platform provides the MessageClient service for sending and receiving messages on a common topic. You can obtain this service from the platform’s bean manager as follows: Beans.get(MessageClient).

Publishing a Message

When publishing a message, the message is transported to all consumers subscribed to the topic. The topic must be exact, thus not contain wildcards. Optionally, you can pass options to control how to publish the message or set message headers. Transfer data to be carried along with the message (i.e. the message body) can be any object which is serializable with the Structured Clone Algorithm.

The following code snippet shows how to publish a message to a topic destination.

  const topic = 'myhome/livingroom/temperature'; (1)

  Beans.get(MessageClient).publish(topic, '22°C'); (2)
1 Specifies the topic where to publish the message to.
2 Looks up the MessageClient from the bean manager and publishes the message.
The method to publish a message returns a Promise that resolves when dispatched the message, or that rejects if the message broker rejected the message.
Receiving Messages

A microfrontend can receive messages published to a topic as following.

  const topic = 'myhome/livingroom/temperature'; (1)

  Beans.get(MessageClient).observe$(topic).subscribe((message: TopicMessage) => {
    console.log(message.body); (2)
  });
1 Specifies the topic which to observe.
2 Prints the body of the received message to the console, which is 22°C in this example.
Subscribing to Multiple Topics Simultaneously

A microfrontend can subscribe to multiple topics simultaneously by using wildcard segments in the topic. If a segment begins with a colon (:), then the segment acts as a placeholder for any segment value. Substituted segment values are then available via the params property of the received message.

For example, subscribing to the topic myhome/:room/temperature receives messages published to the topics myhome/kitchen/temperature and myhome/livingroom/temperature.

  const topic = 'myhome/:room/temperature'; (1)

  Beans.get(MessageClient).observe$(topic).subscribe((message: TopicMessage) => {
    console.log(message.params); (2)
  });
1 Specifies the topic which to observe; the second segment :room is a wildcard segment that matches any value (colon syntax).
2 Prints substituted segment values to the console: {"room" ⇒ "livingroom"} when receiving a message sent to the topic myhome/livingroom/temperature.
Publishing a Retained Message

The platform supports publishing a message as a retained message. Retained messages help newly subscribed clients to get the last message published to a topic immediately upon subscription. The broker stores one retained message per topic, i.e., a later sent retained message will replace a previously sent retained message. To delete a retained message, send a retained message without payload to the topic. Deletion messages are not transported to subscribers.

The following example shows how to publish a message as a retained message.

  const topic = 'myhome/livingroom/temperature';
  Beans.get(MessageClient).publish(topic, '22°C', {retain: true}); (1)
1 Sets the retain flag to true, instructing the message broker to store this message as a retained message.
Publishing a Message with Headers

The platform allows publishing a message with message headers, which the receiver then can read. A message header can contain any data that is serializable with the Structured Clone Algorithm.

  const topic = 'myhome/livingroom/temperature';
  const headers = new Map().set('authorization', 'Bearer <token>'); (1)

  Beans.get(MessageClient).publish(topic, '22°C', {headers: headers});
1 Adds a custom message header.

The recipient can then access the message headers via the headers property on the received message, as illustrated in the following example.

  const topic = 'myhome/livingroom/temperature';

  Beans.get(MessageClient).observe$(topic).subscribe((message: TopicMessage) => {
    console.log(message.headers); (1)
  });
1 Prints received message headers to the console: {"authorization" ⇒ "Bearer <token>"}.

The platform adds some platform-specific headers to each message, like the message id or the publishing timestamp. Platform headers start with the Theta (ɵ) symbol. Some platform headers are public API and available to the application via the enum MessageHeaders.

{"ɵMESSAGE_ID" => "8e53d603-ff12-400b-9d6e-8f41fbbe5f2b"}
{"ɵTIMESTAMP" => 1584124022134}
{"ɵAPP_SYMBOLIC_NAME" => "app-1"}
{"ɵCLIENT_ID" => "c8ce089f-10d7-4c82-b91e-11a7f60c15d3"}
Request-Response Message Exchange

The platform facilitates the request-response message exchange pattern for synchronous communication.

The communication is initiated by the publisher by sending a request (instead of publishing a message). The recipient can then respond to the request. Just as in JMS (Java Message Service), the platform sets a ReplyTo message header on the message, which contains the topic of a temporary inbox where the replier can send replies to. The inbox is destroyed when the publisher (requestor) unsubscribes.

The following code snippet shows how to initiate a request-response communication and receiving replies.

  const topic = 'myhome/livingroom/temperature';

  Beans.get(MessageClient).request$(topic).subscribe(reply => {  (1)
    console.log(reply.body); (2)
  });
1 Initiates a request-response communication by invoking the request$ method on the MessageClient.
2 Prints the received replies to the console.

Similar to publishing a retained message, a request can also be marked as retained, instructing the broker to store it in the broker and deliver it to new subscribers, even if they subscribe after the request has been sent. Unlike retained messages, retained requests are not replaced by later retained requests or messages and remain in the broker until the requestor unsubscribes.

The following code snippet illustrates how to send a retained request.

  const topic = 'myhome/livingroom/temperature';

  Beans.get(MessageClient).request$(topic, undefined, {retain: true}).subscribe(reply => {  (1)
    console.log(reply.body);
  });
1 The retain flag instructs the broker to retain this request until unsubscribed.

In request-response communication, by default, the requestor’s Observable never completes. However, the replier can include the response status code in the reply’s headers, allowing to control the lifecycle of the requestor’s Observable. For example, the status code 250 ResponseStatusCodes.TERMINAL allows completing the requestor’s Observable after emitted the reply, or the status code 500 ResponseStatusCodes.ERROR to error the Observable. See the enum ResponseStatusCodes for available status codes.

If the replier does not complete the communication, the requestor can use the take(1) RxJS operator to unsubscribe upon the receipt of the first reply.

Note that the platform evaluates status codes only in request-response communication. They are ignored when observing topics or intents in pub/sub communication but can still be used; however, they must be handled by the application, e.g., by using the throwOnErrorStatus SCION RxJS operator.


The following code snippet shows how requests are received and answered. You can reply to a request by sending one or more replies to the replyTo topic contained in the request’s headers.

As an illustration, in the following example, a temperature sensor is subscribed to for each request. When the sensor emits the temperature, the temperature is sent to the requester. The sensor Observable is a hot Observable, meaning it never completes, emitting the new temperature with every temperature change.

  const topic = 'myhome/livingroom/temperature';

  // Stream data as long as the requestor is subscribed to receive replies.
  Beans.get(MessageClient).observe$(topic).subscribe((request: TopicMessage) => {
    const replyTo = request.headers.get(MessageHeaders.ReplyTo); (1)

    sensor$
      .pipe(takeUntilUnsubscribe(replyTo)) (3)
      .subscribe(temperature => {
        Beans.get(MessageClient).publish(replyTo, `${temperature}°C`); (2)
      });
  });

  // Alternatively, you can complete the requestor's Observable with the first reply.
  Beans.get(MessageClient).observe$(topic).subscribe((request: TopicMessage) => {
    const replyTo = request.headers.get(MessageHeaders.ReplyTo); (1)
    const headers = new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL); (4)

    sensor$
      .pipe(take(1))
      .subscribe(temperature => {
        Beans.get(MessageClient).publish(replyTo, `${temperature}°C`, {headers});
      });

  });
1 Reads the ReplyTo topic from the request where to send replies to.
2 Sends the temperature to the requestor.
3 Stops replying when the requestor unsubscribes.
4 Sets a message header to immediately complete the requestor’s Observable after emitted the reply.
If streaming data like in the example above, the replier can use the RxJS takeUntilUnsubscribe operator of the platform to stop replying when the requestor unsubscribes.
Convenience API for handling messages

The message client provides the onMessage method as a convenience to the observe$ method. Unlike observe$, messages are passed to a callback function rather than emitted from an Observable. Response(s) can be returned directly from the callback. It supports error propagation and request termination. Using this method over observe$ significantly reduces the code required to respond to requests.

  const topic = 'myhome/livingroom/temperature';

  // Stream data as long as the requestor is subscribed to receive replies.
  Beans.get(MessageClient).onMessage(topic, message => {
    return sensor$.pipe(map(temperature => `${temperature}°C`));
  });

  // Alternatively, you can complete the requestor's Observable with the first reply.
  Beans.get(MessageClient).onMessage(topic, message => {
    return sensor$.pipe(map(temperature => `${temperature}°C`), take(1));
  });

For each message received, the specified callback function is called. When used in request-response communication, the callback function can return the response either directly or in the form of a Promise or Observable. Returning a Promise allows the response to be computed asynchronously, and an Observable allows to return one or more responses, e.g., for streaming data. In either case, when the final response is produced, the handler terminates the communication, completing the requestor’s Observable. If the callback throws an error, or the returned Promise or Observable errors, the error is transported to the requestor, erroring the requestor’s Observable.

3.2.2. Intent-Based Messaging

This chapter introduces the intent-based communication for controlled collaboration between micro applications.


What is Intent-Based Messaging?

Intent-based messaging enables controlled collaboration between micro applications, a mechanism known from Android development where an application can start an activity via an Intent (such as sending an email).

This kind of communication is similar to the topic-based communication, thus implements also the publish-subscribe messaging pattern. Both communication models allow messages to be sent between instances of applications. However, intent-based messaging goes beyond "simple" messaging and is the basis for controlled collaboration. Controlled collaboration means that communication flows are explicit and must be declared in the manifest. For the sending application, this means it must declare an intention in its manifest, and for the receiving application, it must provide a capability. The declaration of capabilities in the manifest file is similar to the OpenAPI specification for REST APIs - it defines the contract for the provided interaction.

We recommend using intent-based communication for cross-application communication to make interaction flows explicit.

The advantage of this kind of communication becomes apparent when the number of cross-application communications grows to provide an overview of the communication flows in the system. For example, in case of a breaking change in the message format, you can clearly identify your communication peers to coordinate the migration. Further, a capability is more than "simply" a messaging endpoint. A capability can be thought of as an API that can be invoked via intent, browsed via the DevTools or programmatically via the Intention API. A capability can specify parameters which must be passed along with the intent. Parameters are part of the contract between the intent publisher and the capability provider.

In topic-based communication, messages are published to a topic destination. In intent-based communication, on the other hand, the destination are capabilities, formulated in an abstract way, consisting of a a type, and optionally a qualifier. The type categorizes a capability in terms of its functional semantics. A capability may also define a qualifier to differentiate the different capabilities of the same type. The type is a string literal and the qualifier a dictionary of key-value pairs. To better understand the concept of the qualifier, a bean manager can be used as an analogy. If there is more than one bean of the same type, a qualifier can be used to control which bean to inject.

The terminology and concepts are explained in more detail in the Intention API chapter. This chapter focuses on communication and illustrates how to send and receive intents using the IntentClient, which is available from the Platform’s bean manager.

Declaring an Intention

An application must declare an intention in its manifest in order to interact with a capability.

To illustrate the concept of intent-based messaging, we will look at an IoT example with sensors controlling room temperature modeled as capabilities. To set the temperature of a room, we send an intent to the room sensor. To get the room temperature, we query the sensor using request-response.

{
  "intentions": [
    {
      "type": "temperature", (1)
      "qualifier": {
        "room": "kitchen"
      }
    }
  ]
}
1 Declares the intention to interact with the temperature sensor in the kitchen.

The type of the intention must exactly match the type of the capability we want to interact with. In specifying the qualifier, we are more flexible. For instance, we can use wildcards to match multiple capabilities with a single intention declaration.

A micro application is implicitly qualified to interact with all its capabilities without declaring an intention.
Using the asterisk wildcard (*) in the qualifier allows matching multiple capabilities with a single intention declaration.

To learn more about an intention, see chapter What is an Intention? in Intention API.

Publishing an Intent

An application can publish intents for intentions declared in its manifest. The platform transports the intent to applications that provide a fulfilling capability. Along with the intent, the application can pass transfer data, either as payload, message headers or parameters. Passed data must be serializable with the Structured Clone Algorithm.

In the following example, we send an intent to the temperature sensor in the kitchen to set the temperature to 22°C.

  const intent: Intent = { (1)
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  Beans.get(IntentClient).publish(intent, '22°C'); (2)
1 Constructs the intent to interact with the temperature sensor in the kitchen.
2 Publishes the intent via IntentClient, obtained from the bean manager.
The method to publish an intent returns a Promise that resolves when dispatched the intent, or that rejects if the message broker rejected the intent.

To learn more about an intent, see chapter What is an Intent? in Intention API.

Publishing a Retained Intent

The platform supports publishing an intent as a retained intent. Retained intents help newly subscribed clients to get the last intent published for a capability immediately upon subscription. The broker stores one retained intent per capability, i.e., a later sent retained intent will replace a previously sent retained intent. To delete a retained intent, send a retained intent without payload to the same destination. Deletion intents are not transported to subscribers.

In the following example, we set the kitchen temperature to 22°C. We send a retained intent since the kitchen temperature sensor may not be available yet. As soon as it connects to the platform, it will receive the intent for adjusting the temperature.

  const intent: Intent = {type: 'temperature', qualifier: {room: 'kitchen'}};

  Beans.get(IntentClient).publish(intent, '22°C', {retain: true}); (1)
1 Sets the retain flag to true, instructing the message broker to store this intent as a retained intent.
Declaring a Capability

An application can make functionality available to other applications via capabilities. Typically, capabilities are declared in the manifest, but can also be contributed programmatically using the ManifestService.

The following code snippet illustrates the declaration of the kitchen temperature sensor as a capability in the manifest.

{
  "capabilities": [
    {
      "description": "Sensor to adjust the room temperature in the kitchen.", (1)
      "type": "temperature", (2)
      "qualifier": { (3)
        "room": "kitchen"
      },
      "private": false, (4)
    }
  ]
}
1 Describes the capability.
2 Categorizes the capability to be a temperature sensor.

The platform provides some platform-specific capability types like Activator or Microfrontend. For custom capabilities, you can choose any name.

3 Qualifies the capability if having multiple temperature sensor capabilities.
4 Makes this a public capability so that other applications can interact with the kitchen sensor.
To have consistency among the qualifiers in the application, we recommend defining guidelines how to name qualifier entries.

To learn more about a capability, see chapter What is a Capability? in Intention API.

Receiving Intents

Intents are transported to applications that provide a fulfilling capability and are typically subscribed to in an activator. An activator is a special microfrontend that an application can provide. Activators are loaded when starting the host application and run for the entire application lifecycle. An activator microfrontend is special in that it is never displayed to the user. Learn more about activator in the chapter Activator.

The following code snippet illustrates how to receive intents.

  const selector: IntentSelector = { (1)
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  Beans.get(IntentClient).observe$(selector) (2)
    .subscribe((message: IntentMessage) => {
      // do something
    });
1 Defines the selector to filter intents. Without a selector, you would receive all intents you have defined a capability for. The selector supports the use of wildcards in the qualifier.
2 Subscribes to intents that match the selector.
An application only receives intents for which it provides a fulfilling capability. The selector is used as a filter only.
Request-Response Message Exchange

The platform facilitates the request-response message exchange pattern for synchronous communication.

The communication is initiated by the requestor by sending a request. The recipient can then respond to the request. Just as in JMS (Java Message Service), the platform sets a ReplyTo message header on the intent, which contains the topic of a temporary inbox where the replier can send replies to. The inbox is destroyed when the requestor unsubscribes.

The following code snippet illustrates how to initiate a request-response communication via intent-based messaging and receiving replies. To explain this type of communication, let’s take a client that requests the current kitchen temperature.

  const intent: Intent = { (1)
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  Beans.get(IntentClient).request$(intent).subscribe(reply => { (1)
    // do something (2)
  });
1 Initiates a request-response communication by invoking the request$ method on the IntentClient.
2 Prints the received temperature to the console.

In request-response communication, by default, the requestor’s Observable never completes. However, the replier can include the response status code in the reply’s headers, allowing to control the lifecycle of the requestor’s Observable. For example, the status code 250 ResponseStatusCodes.TERMINAL allows completing the requestor’s Observable after emitted the reply, or the status code 500 ResponseStatusCodes.ERROR to error the Observable. See the enum ResponseStatusCodes for available status codes.

If the replier does not complete the communication, the requestor can use the take(1) RxJS operator to unsubscribe upon the receipt of the first reply.

Note that the platform evaluates status codes only in request-response communication. They are ignored when observing topics or intents in pub/sub communication.


The following code snippet shows how requests (intents) are received and answered. You can reply to an intent by sending one or more replies to the replyTo topic contained in the intent’s headers. Please note to send replies via the MessageClient.

For illustration purposes, for each request, the following example subscribes to the temperature sensor. The sensor observable never completes and continuously emits when the temperature changes.

  const selector: IntentSelector = {
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  // Stream data as long as the requestor is subscribed to receive replies.
  Beans.get(IntentClient).observe$(selector).subscribe((request: IntentMessage) => {
    const replyTo = request.headers.get(MessageHeaders.ReplyTo); (1)

    sensor$
      .pipe(takeUntilUnsubscribe(replyTo)) (3)
      .subscribe(temperature => {
        Beans.get(MessageClient).publish(replyTo, `${temperature}°C`); (2)
      });
  });

  // Alternatively, you can complete the requestor's Observable with the first reply.
  Beans.get(IntentClient).observe$(selector).subscribe((request: IntentMessage) => {
    const replyTo = request.headers.get(MessageHeaders.ReplyTo); (1)
    const headers = new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL); (4)

    sensor$
      .pipe(take(1))
      .subscribe(temperature => {
        Beans.get(MessageClient).publish(replyTo, `${temperature}°C`, {headers});
      });

  });
1 Reads the ReplyTo topic from the request where to send replies to.
2 Sends the current kitchen temperature to the requestor.
3 Stops replying when the requestor unsubscribes.
4 Sets a message header to immediately complete the requestor’s Observable after emitted the reply.
If streaming data like in the example above, the replier can use the RxJS takeUntilUnsubscribe operator of the platform to stop replying when the requestor unsubscribes.
Convenience API for handling messages

The intent client provides the onIntent method as a convenience to the observe$ method. Unlike observe$, intents are passed to a callback function rather than emitted from an Observable. Response(s) can be returned directly from the callback. It supports error propagation and request termination. Using this method over observe$ significantly reduces the code required to respond to requests.

  const selector: IntentSelector = {
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  // Stream data as long as the requestor is subscribed to receive replies.
  Beans.get(IntentClient).onIntent(selector, intentMessage => {
    return sensor$;
  });

  // Alternatively, you can complete the requestor's Observable with the first reply.
  Beans.get(IntentClient).onIntent(selector, intentMessage => {
    return sensor$.pipe(take(1));
  });

For each intent received, the specified callback function is called. When used in request-response communication, the callback function can return the response either directly or in the form of a Promise or Observable. Returning a Promise allows the response to be computed asynchronously, and an Observable allows to return one or more responses, e.g., for streaming data. In either case, when the final response is produced, the handler terminates the communication, completing the requestor’s Observable. If the callback throws an error, or the returned Promise or Observable errors, the error is transported to the requestor, erroring the requestor’s Observable.

3.2.3. Intercepting Messages

The platform provides a common mechanism to intercept messages or intents before their publication using interceptors.


How to Intercept Messages

An interceptor can reject or modify messages, allowing you to audit messages, or to perform schema validation to reject messages with an invalid payload. Multiple interceptors can be registered, forming a chain in which each interceptor is called one by one in registration order.

interceptor chain

An interceptor must implement the intercept method of the MessageInterceptor for intercepting messages sent to a topic. Intercepting intents is similar, just implement IntentInterceptor instead. For each message or intent sent, the platform invokes the intercept method of the first registered interceptor, passing the message and the next handler as arguments. By calling the next handler in the intercept method, message dispatching is continued. If there is no more interceptor in the chain, the message is transported to the receivers, if any. But, if throwing an error in the intercept method, message dispatching is aborted, and the error transported back to the sender.

  /** Message Interceptor */
  class MessageLoggerInterceptor implements MessageInterceptor {

    public intercept(message: TopicMessage, next: Handler<TopicMessage>): Promise<void> {
      console.log(message);

      // Passes the message along the interceptor chain.
      return next.handle(message);
    }
  }

  /** Intent Interceptor */
  class IntentLoggerInterceptor implements IntentInterceptor {

    public intercept(intent: IntentMessage<any>, next: Handler<IntentMessage>): Promise<void> {
      console.log(intent);

      // Passes the intent along the interceptor chain.
      return next.handle(intent);
    }
  }
Registering Interceptors

You register interceptors with the bean manager before starting the platform. Interceptors can be registered only in the host application. They are invoked in registration order.

  Beans.register(MessageInterceptor, {useClass: MessageLoggerInterceptor, multi: true}); (1)
  Beans.register(IntentInterceptor, {useClass: IntentLoggerInterceptor, multi: true}); (2)

  // Start the platform.
  MicrofrontendPlatformHost.start(...); (3)
1 Registers a message interceptor when starting the platform.
2 Registers an intent interceptor when starting the platform.
3 Starts the platform.
Message interceptors and intent interceptors are registered as multi beans under the token MessageInterceptor or IntentInterceptor, respectively. For more information about bean registration, see chapter Bean Manager.
Filtering Messages for Interception

The platform passes all messages to the interceptors, including platform messages vital for its operation. Interceptors must filter messages on their own, e.g. by topic, and let all other messages pass.

For message filtering, you can use the TopicMatcher, allowing you to test whether a topic matches a pattern. The pattern must be a topic, not a regular expression; thus, it must consist of one or more segments, each separated by a forward slash. The pattern can contain wildcard segments. Wildcard segments start with a colon (:), acting act as a placeholder for any segment value.

  const pattern = 'product/:id';

  // Matches the topic 'product/123' against the pattern 'product/:id'
  const positiveMatch = new TopicMatcher(pattern).match('product/123');
  console.log(positiveMatch.matches); // true
  console.log(positiveMatch.params); // {id => 123}

  // Matches the topic 'person/123' against the pattern 'product/:id'
  const negativeMatch = new TopicMatcher(pattern).match('person/123');
  console.log(negativeMatch.matches); // false
Implementing a Message Validator

A typical use case for an interceptor is to perform schema validation to reject messages with an invalid payload.

The following code snippet illustrates how to implement an interceptor to validate the payload of messages. The interceptor expects a filter topic and JSON schema for its construction. The interceptor validates all messages that match the given filter topic against the given JSON schema.

JSON message validation is out of scope of the platform. A good library is AJV (Another JSON Schema Validator). For illustrative purposes, the below code uses the JsonSchemaValidator pseudo class.
  class MessageValidatorInterceptor implements MessageInterceptor {

    private topicMatcher: TopicMatcher;
    private schemaValidator: JsonSchemaValidator;

    constructor(topic: string, jsonSchema: any) {
      this.topicMatcher = new TopicMatcher(topic); (1)
      this.schemaValidator = new JsonSchemaValidator(jsonSchema); (2)
    }

    public intercept(message: TopicMessage, next: Handler<TopicMessage>): Promise<void> {
      // Pass messages sent to other topics.
      if (!this.topicMatcher.match(message.topic).matches) {
        return next.handle(message); (3)
      }

      // Validate the payload of the message.
      if (this.schemaValidator.isValid(message.body)) {
        return next.handle(message); (4)
      }

      throw Error('Message failed schema validation'); (5)
    }
  }
1 Constructs the topic matcher to filter messages subject for validation.
2 Constructs the JSON validator to validate messages against given JSON schema.
3 Lets messages pass that do not match the topic.
4 Lets messages with a valid JSON payload pass.
5 Rejects messages if not passing JSON validation. The platform sends the error back to the sender.

As this interceptor requires a filter topic and JSON schema as its constructor arguments, we register it in the bean manager using a factory function, as shown below.

  // Register interceptor to validate product related messages
  Beans.register(MessageInterceptor, {
    useFactory: (): MessageInterceptor => {
      const productTopic = 'products/:id'; (1)
      const productJsonSchema = import('./product.schema.json'); (2)
      return new MessageValidatorInterceptor(productTopic, productJsonSchema); (3)
    },
    multi: true,
  });
1 Specifies the topic to filter messages for validation. The topic contains the product id (:id) as wildcard segment.
2 Loads the JSON schema using a dynamic import.
3 Constructs the interceptor.

3.3. Intention API

This chapter introduces the concepts of the Intention API.


3.3.1. Concepts and Usage

The Intention API enables controlled collaboration between micro applications. It is inspired by the Android platform where an application can start an activity via an Intent (such as sending an email).

To collaborate, an application must express an intention. An intention refers to one or more capabilities, or activity, in the Android platform. Capabilities can be browsed similar to a catalog and invoked by issuing an intent. Manifesting intentions enables us to see dependencies between applications down to the functional level.

A micro application can make functionality available to micro applications in the form of capabilities. For a micro application to browse or invoke a capability, the micro application must declare an intention in its manifest. To invoke a capability, a micro application issues an intent.

intention api

Both capabilities and intentions need to be declared in the manifest. For example, capabilities can be browsed to create dynamic page content, such as a toolbar with items contributed via capabilities. When the user clicks an item of the toolbar, the application can issue an intent, which the platform transports to the application providing the capability.

3.3.2. The Manifest

The manifest is a special file that contains information about a micro application. A micro application declares its intentions and capabilities in its manifest file. The manifest needs to be registered in the host application via URL.

The following code snippet shows the manifest of the Product Catalog Application.

{
  "name": "Product Catalog Application",
  "baseUrl": "#",
  "capabilities": [
    {
      "description": "Shows the product list microfrontend.",
      "type": "microfrontend",
      "qualifier": {
        "entity": "product-list"
      }
      "properties": {
        "path": "/products",
      }
    },
    {
      "description": "Microfrontend to display a product.",
      "type": "microfrontend",
      "qualifier": {
        "entity": "product"
      }
      "params": [
        {"name":"id", "required": true},
      ],
      "properties": {
        "path": "/products/:id",
      }
    }
  ],
  "intentions": [
    {
      "type": "microfrontend",
      "qualifier": {
        "entity": "customer-review"
      }
    }
  ]
}
Properties of Manifest
Property Type Mandatory Description

name

string

yes

The name of the application, e.g. displayed in the Developer Tools.

baseUrl

string

no

URL to the application root. The URL can be fully qualified, or a path relative to the origin under which serving the manifest file. If not specified, the origin of the manifest file acts as the base URL. The platform uses the base URL to resolve microfrontends such as activator endpoints.
For a Single Page Application that uses hash-based routing, you typically specify the hash symbol (#) as the base URL.

capabilities

no

Functionality that qualified micro application can look up or call via intent.

intentions

no

Functionality which this micro application intends to use.

3.3.3. What is a Capability?

A capability can be thought of as an API that an application provides that can be invoked via intent, browsed via the DevTools or programmatically via the Intention API. Capabilities must be registered in the manifest or contributed programmatically via ManifestService.

A capability is formulated in an abstract way consisting of a type and optionally a qualifier. The type categorizes a capability in terms of its functional semantics (e.g., microfrontend if providing a microfrontend). Multiple capabilities can be of the same type. In addition to the type, a capability can define a qualifier to differentiate the different capabilities of the same type. A qualifier is a dictionary of arbitrary key-value pairs.

Data ownership is an important rule to follow when implementing a microservices/microfrontend architecture. Therefore, adding the domain entity name to the qualifier is an easy way to uniquely address capabilities.
To have consistency among the qualifiers in the entire application, we recommend defining guidelines how to name qualifier entries.

A capability can have private or public visibility. If private, which is by default, the capability is not visible to other micro applications; thus, it can only be invoked or browsed by the providing micro application.

A capability can specify parameters which the intent issuer can/must pass along with the intent. Parameters are part of the contract between the intent publisher and the capability provider. They do not affect the intent routing, unlike the qualifier.

Metadata can be associated with a capability in its properties section. For example, if providing a microfrontend, the URL to the microfrontend can be added as property, or if the capability contributes a toolbar item, its label to be displayed.

The following code snippet shows an example of how to declare a capability in the manifest of an application.

{
  "description": "Sensor to adjust the room temperature in the kitchen.", 
  "type": "temperature",
  "qualifier": {
    "room": "kitchen"
  },
  "params": [
    {"name":"authorization", "required": false},
  ],
  "private": false,
  "properties": {
    "floor": "first floor",
  }
}
Properties of Capability
Property Type Mandatory Description

type

string

yes

Categorizes the capability in terms of its functional semantics (e.g., microfrontend if providing a microfrontend). There are a handful of platform-specific capability types, such as activator or microfrontend. For custom capabilities, you can choose any name.

qualifier

Dictionary

no

The qualifier is a dictionary of key-value pairs to differentiate capabilities of the same type. The qualifier is like an abstract description of the capability. It should include enough information to uniquely identify the capability.

Intents must exactly match the qualifier of the capability, if any.

params

ParamDefinition[]

no

Specifies parameters which the intent issuer can/must pass along with the intent.

Parameters are part of the contract between the intent publisher and the capability provider. They do not affect the intent routing, unlike the qualifier. A parameter needs to declare its name and whether it is required or optional, as follows: {"name":"param","required": true}. Optionally, a description and deprecation notes can be specified, or additional metadata to be interpreted in an interceptor.

private

boolean

no

Controls if this capability is visible to other micro applications. If private, which is by default, the capability is not visible to other micro applications; thus, it can only be invoked or browsed by the providing micro application.

description

string

no

A short description to explain the capability.

properties

Dictionary

no

Arbitrary metadata to be associated with the capability.

In addition to declaring a capability in the manifest, the micro application typically subscribes to intents in an activator. Refer to chapter Activator for more information.

The following code snippet shows an example of how an application can subscribe to intents.

  const selector: IntentSelector = {
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  Beans.get(IntentClient).observe$(selector).subscribe((message: IntentMessage) => {
    // handle the intent
  });
A micro application only receives intents for which it provides a fulfilling capability. The selector is used as a filter only.

For more information about handling an intent, see the chapter Receiving Intents in Intent-Based Messaging.

3.3.4. What is an Intention?

An intention refers to one or more capabilities that an application wants to interact with. Intentions are declared in the application’s manifest and are formulated in an abstract way, consisting of a type and optionally a qualifier. The qualifier is used to differentiate capabilities of the same type.

If providing a capability, the providing application is implicitly qualified to interact with its capability, thus needs not to declare an intention.

The following code snippet shows an example of how to declare an intention in the manifest of an application.

{
  "type": "temperature",
  "qualifier": {
    "room": "kitchen"
  }
}
Properties of Intention
Property Type Mandatory Description

type

string

yes

The type of capability to interact with.

qualifier

Dictionary

no

Qualifies the capability which to interact with.

The qualifier is a dictionary of arbitrary key-value pairs to differentiate capabilities of the same type.

The intention must exactly match the qualifier of the capability, if any. The intention qualifier allows using wildcards to match multiple capabilities simultaneously.

  • Asterisk wildcard character (*):
    Matches capabilities with such a qualifier property no matter of its value (except null or undefined). Use it like this: {property: '*'}.

  • Partial wildcard (**):
    Matches capabilities even if having additional properties. Use it like this: {'*': '*'}.

3.3.5. What is an Intent?

The intent is the message that a micro application sends to interact with functionality that is available in the form of a capability.

An intent consists of a type and optionally a qualifier, which are used by the platform to identify the capability(s) to which to transport the intent. A micro application can issue an intent only if having declared a respective intention in its manifest. Along with the intent, you can pass transfer data, either as payload, message headers or parameters. Passed data must be serializable with the Structured Clone Algorithm.

The following code snippet illustrates how to send an intent.

  const intent: Intent = {
    type: 'temperature',
    qualifier: {room: 'kitchen'},
  };

  Beans.get(IntentClient).publish(intent, '22°C');

The following code snippet illustrates how to pass parameters with an intent. Passed parameters must be defined in the capability, otherwise the intent would be rejected.

In the following example, we use an authorization param to pass along the authorization token.

  const intent: Intent = {
    type: 'temperature',
    qualifier: {room: 'kitchen'},
    params: new Map().set('authorization', 'Bearer <token>')
  };

  Beans.get(IntentClient).publish(intent, '22°C');

For more information about publishing an intent, see the chapter Publishing an Intent in Intent-Based Messaging.

3.3.6. Browsing Capabilities

A micro application can browse the catalog of capabilities using the ManifestService.

lookup capabilities

Looking up capabilities allows the flexible composition of web content, such as populating a toolbar with items provided in the form of capabilities. A micro application can look up its own capabilities and public capabilities for which it has declared an intention.

In below example, a toolbar is composed of toolbar items contributed by micro applications in the form of capabilities. When the user clicks a toolbar item, the integrating app issues an intent, which the platform then transports to the providing micro application.

capability contribution

The implementor of the toolbar can lookup toolbar item capabilities as follows:

  // Filter toolbar item capabilities.
  const toolbarItemFilter: ManifestObjectFilter = {
    type: 'menu-item',
    qualifier: {
      location: 'toolbar:main',
      action: '*',
    },
  };

  Beans.get(ManifestService).lookupCapabilities$(toolbarItemFilter).subscribe(toolbarItems => { (1)
    // Clear toolbar list.
    const toolbarElement = document.querySelector('ul.toolbar.main');
    toolbarElement.innerHTML = '';

    // For each toolbar item capability, create a toolbar list item.
    toolbarItems.forEach(toolbarItem => {
      const toolbarItemElement = toolbarElement.appendChild(document.createElement('li'));

      // Set the label as specified in the capability properties.
      toolbarItemElement.innerText = toolbarItem.properties.label;

      // Issue an intent when clicking the toolbar item.
      toolbarItemElement.addEventListener('click', () => {
        const intent = {type: toolbarItem.type, qualifier: toolbarItem.qualifier};
        Beans.get(IntentClient).publish(intent);  (2)
      });
    });
  });
1 Looks up toolbar items for the main application toolbar.
2 Issues an intent when the user clicks a toolbar item. For the intent, we use the type and qualifier of the capability. The micro application providing the capability does then handle the intent.
To see how to handle intents in the capability providing micro application, see the chapter Receiving Intents in Intent-Based Messaging.

By passing a ManifestObjectFilter to the lookupCapabilities$ method, you can control which capabilities to observe. Specified filter criteria are "AND"ed together. If not passing a filter, all capabilities visible to the requesting micro application are observed. When subscribing to the Observable, it emits the requested capabilities. The Observable never completes and emits continuously when fulfilling capabilities are registered or unregistered.

A micro application can only look up its own capabilities and public capabilities for which it has declared an intention.
Properties of ManifestObjectFilter to filter capabilities
Property Type Mandatory Description

id

string

no

Observes a single capability of the given identity.

type

string

no

Observes capabilities of a specific type.

qualifier

Dictionary

no

Observes capabilities that match the qualifier.

If specifying a qualifier filter, the capabilities must match that filter exactly. The filter supports the asterisk wildcard to match any value, e.g., {property: '*'}, or partial matching to find capabilities with at least the specified qualifier properties. Partial matching is enabled by appending the any-more entry to the qualifier, as following: {'*': '*'}.

appSymbolicName

string

no

Observes capabilities that are provided by a specific micro application.

3.3.7. Browsing Intentions

The platform allows you to browse and observe intentions. Unlike browsing capabilities, an application can browse the intentions of all micro applications. The use case for browsing intentions is somewhat technical, e.g., used by the DevTools to list intentions declared by micro applications.

You can browse intentions using the ManifestService.lookupIntentions$ method. By passing a ManifestObjectFilter to the lookupIntentions$ method, you can control which intentions to observe. Specified filter criteria are "AND"ed together. If not passing a filter, all intentions are observed. When subscribing to the Observable, it emits the requested intentions. The Observable never completes and emits continuously when matching intentions are registered or unregistered.

Properties of ManifestObjectFilter to filter intentions
Property Type Mandatory Description

id

string

no

Observes a single intention of the given identity.

type

string

no

Observes intentions of a specific type.

qualifier

Dictionary

no

Observes intentions that match the qualifier.

If specifying a qualifier filter, the intentions must match that filter exactly. The filter supports the asterisk wildcard to match any value, e.g., {property: '*'}, or partial matching to find intentions with at least the specified qualifier properties. Partial matching is enabled by appending the any-more entry to the qualifier, as following: {'*': '*'}.

appSymbolicName

string

no

Observes intentions that are provided by a specific micro application.

3.3.8. Registering Capabilities

Capabilities can be declared statically via the manifest file or registered at runtime via the ManifestService. Registering or unregistering capabilities at runtime enables a micro application to contribute functionality more flexibly. For example, a micro application could inform the user about an upcoming maintenance window by temporarily registering a notification capability.

register capabilities

The following code snippet illustrates how to register a capability dynamically.

    // Inform the user about planned maintenance.
    const capabilityId = await Beans.get(ManifestService).registerCapability({ (1)
      type: 'notification',
      description: 'Informs the user about planned system maintenance',
      private: false,
      properties: {
        service: 'Payment',
        message: `Due to planned system maintenance, paying by credit card on this Sunday, 29 August,
                  between 22:00 and 23:00 CET is not possible. Thank you for your understanding.`,
      },
    });

    // Clear the notification after 30 seconds.
    setTimeout(() => {
      Beans.get(ManifestService).unregisterCapabilities({id: capabilityId}); (2)
    }, 30000);
1 Registers the capability to inform the user about planned maintenance.
2 Unregisters the capability after 30 seconds.

For an overview of the supported capability properties, see chapter Properties of Capability.

Capabilities are typically registered in an activator. An activator is a special microfrontend that a micro application can provide to interact with the platform. Activators are loaded when starting the host application and run for the entire application lifecycle. For more information, refer to chapter Activator.

The host application, for example, can then observe these user-notification capabilities and display the message to the user.

  const filter: ManifestObjectFilter = {type: 'notification'};
  Beans.get(ManifestService).lookupCapabilities$(filter).subscribe(notifications => { (1)
    // Clear present notifications.
    const notificationList = document.querySelector('ul.notifications');
    notificationList.innerHTML = '';

    // Show each notification to the user.
    notifications.forEach(notification => {
      const notificationElement = notificationList.appendChild(document.createElement('li'));
      notificationElement.innerText = `${notification.properties.service}:
                                       ${notification.properties.message}`; (2)
    });
  });
1 Observes notification capabilities.
2 Displays each notification.

For more information about how observing capabilities, see chapter Browsing Capabilities.

3.3.9. Registering Intentions

The platform allows registering or unregistering intentions at runtime. Note that this API is disabled by default.

It is strongly discouraged to enable this API for a micro application. Instead, micro applications should declare their required functionality upfront in their manifest and use wildcards in their intention declarations. Otherwise, the advantage of the Intention API would be weakened, since you no longer can statically inspect requirements of applications.

To enable this API for a specific micro application, unset the flag intentionRegisterApiDisabled when registering the app.

  MicrofrontendPlatformHost.start({
    applications: [
      {
        symbolicName: 'product-catalog-app',
        manifestUrl: 'https://product-catalog.webshop.io/manifest.json',
        intentionRegisterApiDisabled: false, (1)
      },
    ],
  });
1 Enables the API for the Product Catalog Application.

Similar to registering a capability, you can register an intention using the ManifestService and its registerIntention method. For an overview of the supported intention properties, see chapter Properties of Intention.

3.3.10. Intercepting Capabilities

The platform allows intercepting capabilities before they are registered, for example, to perform validation checks, add metadata, or change properties.

An interceptor must implement the intercept method of the CapabilityInterceptor interface. Interceptors are registered in the bean manager of the host application under the symbol CapabilityInterceptor as multi bean. Multiple interceptors can be registered, forming a chain in which each interceptor is called one by one in registration order.

  Beans.register(CapabilityInterceptor, {useClass: MicrofrontendCapabilityInterceptor}); (1)

  // Start the platform.
  MicrofrontendPlatformHost.start(...); (2)
1 Registers an interceptor to intercept capabilities before they are registered in the platform.
2 Starts the platform.

The following interceptor assigns a stable identifier to each microfrontend capability.

  class MicrofrontendCapabilityInterceptor implements CapabilityInterceptor {

    public async intercept(capability: Capability): Promise<Capability> {
      if (capability.type === 'microfrontend') {
        return {
          ...capability,
          metadata: {...capability.metadata, id: hash(capability)},
        };
      }
      return capability;
    }
  }

3.4. Embedding Microfrontends

This chapter describes how you can embed microfrontends in the host application and a micro application.

With SCION Microfrontend Platform, you can integrate any web page as a microfrontend using an iframe. If the microfrontend wants to interact with other microfrontends, you need to register it as a micro application in the host application.

The web page to embed must not have the HTTP header 'X-Frame-Options' set because the browser would prevent its integration otherwise.

3.4.1. Router Outlet

The Router Outlet is a web component that allows embedding web content using the router.


Concepts and Usage

Embedding web content using an iframe can quickly become a daunting task. For this reason, the SCION Microfrontend Platform provides a router outlet that solves many of the cumbersome quirks of iframes.

The router outlet is a web component that wraps an iframe. It can be used like a native HTML element. As its name suggests, the web content of the outlet is controlled by a router. The router is a platform service, allowing you to set the URL of an outlet from anywhere in the application, even across application boundaries. When adding the outlet to the DOM, the outlet displays the last URL routed for it, if any. When repeating routing for an outlet, its content is replaced.

The router outlet can be added to a HTML page as follows.

  <sci-router-outlet name="aside"></sci-router-outlet>

To display web content in the outlet, navigate to the URL using the router, as follows:

  Beans.get(OutletRouter).navigate('https://micro-frontends.org', {outlet: 'aside'});

As an alternative to navigating directly to a URL, the router supports navigation to a microfrontend capability via an intent. For more information, refer to Navigating via Intent. Routing is explained in more detail in chapter Routing.

If no content is routed for display in the router outlet, the CSS class sci-empty is added to the outlet. An outlet does not display content if no navigation has taken place yet, or if the outlet content has been cleared.

The host application typically adds one or more top-level router outlets to its main application shell.

top level router outlets
1 PRIMARY outlet to display the main content
2 ASIDE outlet to display context sensitive content

Outlets can be nested, allowing a microfrontend to embed another microfrontend. There is no limit to the number of nested outlets. However, be aware that nested content is loaded cascaded, that is, only loaded once its parent content finished loading.

The following figure shows a microfrontend that embeds another microfrontend.

nested router outlets
Outlet size

The router outlet can adapt its size to the preferred size of its embedded content. The preferred size is set by the microfrontend embedded in the router outlet, which, therefore, requires the embedded microfrontend to be connected to the platform. For detailed instructions on how to register a micro application and connect to the plaform, refer to the chapter Configuration and Startup.

The preferred size of an element is the minimum size that allows it to display normally. Setting a preferred size is useful if the outlet is displayed in a layout that aligns its items based on the items' content size.

Embedded content can report its preferred size using the PreferredSizeService, causing the outlet to change its size.

  Beans.get(PreferredSizeService).setPreferredSize({width: '100%', minHeight: '400px'});

In addition to explicitly setting the preferred size, the platform provides a convenience API to bind a DOM element via PreferredSizeService.fromDimension to automatically report its content size as preferred size to the outlet.

  const mainElement = document.querySelector('main');

  // Bind the element to automatically report its size.
  Beans.get(PreferredSizeService).fromDimension(mainElement);

Prerequisites for the element used as outlet size provider:

  • The element to be observed via PreferredSizeService.fromDimension must behave as block-level box and not as inline-level box. So, if you want to observe an inline element, set its display type to either block or inline-block.

  • If the element to be observed should not fill the remaining space and may change in size, we recommend taking it out of the document element flow, i.e., position it absolutely without defining a width and height. Otherwise, once the element has reported a preferred size, it could not shrink below that size.

Scrollable Content

By default, page scrolling is enabled for the embedded content, displaying a scrollbar when it overflows. If disabled, overflowing content is clipped, unless the embedded content uses a viewport, or reports its preferred size to the outlet.

The below code snippet illustrates how to disable page scrolling for the embedded content.

  <sci-router-outlet scrollable="false"></sci-router-outlet>
Keystroke Bubbling

The router outlet allows the registration of keystrokes, instructing embedded content at any nesting level to propagate corresponding keyboard events to this outlet.

The outlet dispatches keyboard events for registered keystrokes as synthetic, untrusted keyboard events via its event dispatcher. They bubble up the DOM tree like regular events. Propagated events are of the original type, meaning that when the user presses a key on the keyboard, a keydown keyboard event is dispatched, or a keyup event when releasing a key, respectively.

A keystroke has the following syntax.

keystroke syntax

A keystroke is a string that has several parts, each separated with a dot. The first part specifies the event type (keydown or keyup), followed by optional modifier part(s) (alt, shift, control, meta, or a combination thereof) and with the keyboard key as the last part. The key is a case-insensitive value of the KeyboardEvent.key property. Two keys are an exception to the value of the KeyboardEvent.key property: dot and space.

The keystroke behavior can be controlled via flags, a dictionary of key-value pairs. Flags are optional. Multiple flags are separated by a semicolon. The following flags are supported:

Supported keystroke flags
Flag Supported Values Default if not set Description Example

preventDefault

true, false

false

allows preventing the browser’s default action

{preventDefault=true}

You can register keystrokes on a <sci-router-outlet> as follows. Multiple keystrokes are separated with a comma.

  <sci-router-outlet keystrokes="keydown.escape,keydown.control.alt.enter,keydown.control.space">
  </sci-router-outlet>

Alternatively, you can register keystrokes on the DOM element as shown below.

  const outlet: SciRouterOutletElement = document.querySelector('sci-router-outlet');
  outlet.keystrokes = [
    'keydown.escape',
    'keydown.control.alt.enter',
    'keydown.control.space',
  ];
Router Outlet Events

The router outlet emits the following events as custom DOM events. You can attach an event listener declaratively in the HTML template using the onevent handler syntax, or programmatically using the addEventListener method.

activate

The activate custom DOM event is fired when a microfrontend is mounted. It contains the URL of the mounted microfrontend in its details property as string value. The microfrontend may not be fully loaded yet.

deactivate

The deactivate custom DOM event is fired when a microfrontend is about to be unmounted. It contains the URL of the unmounted microfrontend in its details property as string value.

focuswithin

The focuswithin custom DOM event is fired when the microfrontend loaded into the outlet, or any of its child microfrontends, has gained or lost focus. It contains the current focus-within state in its details property as a boolean value: true if focus was gained, or false if focus was lost.

The event does not bubble up through the DOM. After gaining focus, the event is not triggered again until the embedded content loses focus completely, i.e., when focus does not remain in the embedded content at any nesting level. This event behaves like the :focus-within CSS pseudo-class but operates across iframe boundaries. For example, it can be useful when implementing overlays that close upon focus loss.

Note that SCION can only monitor the focus of microfrontends that are connected to the platform.
Examples for subscribing to router-outlet events

The following example attaches an event listener in the HTML template.

  <sci-router-outlet onfocuswithin="onFocusWithin()"></sci-router-outlet>

For an Angular application, it would look as follows:

  <sci-router-outlet (focuswithin)="onFocusWithin($event)"></sci-router-outlet>

The example below adds an event listener programmatically.

  const outlet: SciRouterOutletElement = document.querySelector('sci-router-outlet');
  outlet.addEventListener('focuswithin', (event: CustomEvent) => console.log(`focuswithin: ${event.detail}`));
Outlet Context

The router outlet allows associating contextual data, which then is available to embedded content at any nesting level. Data must be serializable with the structured clone algorithm. Embedded content can look up contextual data using the ContextService. Typically, contextual data is used to provide microfrontends with information about their embedding environment.

Each outlet spans a new context. A context is like a Map with key-value entries. Contexts form a hierarchical tree structure. When looking up a value and if the value is not found in the current context, the lookup is retried on the parent context, repeating until either a value is found, or the root of the context tree has been reached.


Imagine a tabbar with tabs implemented as a microfrontend. As an example, a microfrontend should highlight its tab in the tabbar when its data changes. For each tab, you can define a random highlighting topic and put it to the context of the tab router outlet. The microfrontend can then send an event to that topic when its data changes.

  const highlightingTopic = UUID.randomUUID(); (1)

  const tabOutlet: SciRouterOutletElement = document.querySelector('sci-router-outlet');
  tabOutlet.setContextValue('highlighting-topic', highlightingTopic); (2)

  // Subscribe to highlighting events to highlight the tab
  Beans.get(MessageClient).observe$(highlightingTopic).subscribe(() => {
    // highlight the tab
  });
1 Generates some random UUID.
2 Puts the UUID to the context.

Embedded microfrontend can then read the highlighting-topic from the current context and send an event to that topic when its data changes.

    const highlightingTopic = await Beans.get(ContextService).lookup<string>('highlighting-topic'); (1)

    /** Method invoked when data of the microfrontend changes. */
    function onMicrofrontendDataChange(): void {
      Beans.get(MessageClient).publish(highlightingTopic); (2)
    }
1 Looks up the highlighting-topic from the current context.
2 Sends an event to the highlighting-topic when its data changes.
Splash

Loading and bootstrapping a microfrontend can take some time, at worst, only displaying content once initialized. To indicate the loading of a microfrontend, the navigator can instruct the router outlet to display a splash until the microfrontend signals readiness.

  Beans.get(OutletRouter).navigate('path/to/microfrontend', {showSplash: true});

The splash is the markup between the opening and closing tags of the router outlet element.

  <sci-router-outlet>
    Loading...
  </sci-router-outlet>

The splash is displayed until the embedded microfrontend signals readiness.

  MicrofrontendPlatformClient.signalReady();

To lay out the content of the splash use the pseudo-element selector ::part(splash).

Example of centering splash content in a CSS grid container:

  sci-router-outlet::part(splash) {
    display: grid;
    place-content: center;
  }
If the application explicitly sets the CSS color scheme (e.g., because of support for different themes), we recommend showing a splash. Otherwise, if the microfrontend and the host use different color schemes, the iframe is no longer transparent, causing unwanted flickering particularly while loading the microfrontend. See https://github.com/w3c/csswg-drafts/issues/4772#issuecomment-591553929 for more information.
Router Outlet API

You can find the full list of available properties and methods of SciRouterOutletElement in the TypeDoc.

To hide inherited properties and methods of the HTMLElement in the TypeDoc, uncheck the Inherited checkbox in the upper right corner of the TypeDoc.

3.4.2. Routing

Routing refers to the navigation in a router outlet using the router of the SCION Microfrontend Platform.


Concepts and Usage

In SCION Microfrontend Platform, routing means instructing a <sci-router-outlet> to display the content of a URL. Routing works across microfrontend and micro application boundaries, allowing the URL of an outlet to be set from anywhere in the application. The web content displayed in an outlet can be any HTML document that has not set the HTTP header X-Frame-Options. Routing is also referred to as navigating.

The router supports multiple outlets in the same application to co-exist. By giving an outlet a name, you can reference it as the routing target. If not naming an outlet, its name defaults to primary. If multiple outlets have the same name, they all show the same content. If routing in the context of a router outlet, that is inside a microfrontend, and not specifying a routing target, the content of the current outlet is replaced.

An outlet does not necessarily have to exist at the time of routing. When adding the outlet to the DOM, the outlet displays the last URL routed for it. When repeating routing for an outlet, its content is replaced.

A router outlet is defined as follows. If no navigation has been performed for the outlet yet, then its content is empty.

  <sci-router-outlet name="aside"></sci-router-outlet>

The router supports navigating via URL or intent as described below.

Navigating via URL

The URL of the page to be loaded into the router outlet is passed to the router, as follows:

  Beans.get(OutletRouter).navigate('https://micro-frontends.org', {
    outlet: 'aside', (1)
  });
1 Specifies the routing target. If not specifying an outlet and if navigating in the context of an outlet, that outlet will be used as the navigation target, or the primary outlet otherwise.
Relative Navigation

The router allows to use both absolute and relative paths. A relative path begins with a navigational symbol /, ./, or ../. By default, relative navigation is relative to the current window location of the navigating application, unless specifying a base path for the navigation.

  // Navigation relative to the root path segment
  Beans.get(OutletRouter).navigate('/products/:id', {outlet: PRIMARY_OUTLET});

  // Navigation relative to the parent path segment
  Beans.get(OutletRouter).navigate('../products/:id', {outlet: PRIMARY_OUTLET});

  // Navigation relative to https://product-catalog.webshop.io/
  Beans.get(OutletRouter).navigate('/products/:id', {
    outlet: PRIMARY_OUTLET,
    relativeTo: 'https://product-catalog.webshop.io/',
  });
Named URL Parameters

The URL being passed to the router can contain named parameters which the router replaces with values of the passed params object. A named parameter begins with a colon (:) and is allowed in path segments, query parameters, matrix parameters and the fragment part, e.g., product/:id or product;id=:id or products?id=:id.

  Beans.get(OutletRouter).navigate('/products/:id', { (1)
    outlet: 'aside', (2)
    params: new Map().set('id', '500f3dba-a638-4d1c-a73c-d9c1b6a8f812'), (3)
  });
1 Instructs the outlet router to load the page /products/:id. If not specifying an absolute URL, the path is relative to the base URL of the micro application as specified in the manifest.
2 Specifies in which router outlet to display the page.
3 Passes params for named parameter substitution. In this example, the URL contains the named path segment :id, which the router replaces with 123.
Navigating via Intent

As an alternative to navigating directly to a URL, the router supports navigation to a microfrontend capability via an intent. We refer to this as intent-based routing.

We recommend using intent-based routing over url-based routing, especially for cross-application navigations, since the navigation flows are explicit, i.e., declared in the manifest, and to keep the microfrontend URLs an implementation detail of the micro applications that provide the microfrontends.

If the microfrontend is provided by another micro application, the navigating app must manifest an intention. Also, the navigating app can only navigate to public microfrontend capabilities. See chapter What is an Intention? for more information.

The following code snippet illustrates how to display the product microfrontend in the aside outlet. Note that you only need to pass the qualifier of the microfrontend capability and not its type. The capability type, which is always microfrontend, is implicitly set by the router.

  Beans.get(OutletRouter).navigate({entity: 'product'}, { (1)
    outlet: 'aside', (2)
    params: new Map().set('id', 123), (3)
  });
1 Qualifies the microfrontend which to load into the outlet.
2 Specifies the routing target. See Outlet Resolution Rules which outlet is used if not specifying an outlet.
3 Passes parameters as defined by the microfrontend capability. If not passing all required parameters, the router throws an error.
Providing a Microfrontend Capability

Applications can provide microfrontend capabilities through their manifest. A microfrontend can be either application private or exposed to other micro applications. The platform requires all microfrontend capabilities to be of type microfrontend. A particular microfrontend can be identified using its qualifier.

  {
    "type": "microfrontend", (1)
    "qualifier": {
      "entity": "product" (2)
    },
    "description": "Displays a product.", (3)
    "params": [
      {"name": "id", "required": true} (4)
    ],
    "private": false, (5)
    "properties": { (6)
      "path": "product/:id", (7)
    }
  }  
1 Categorizes the capability as a microfrontend.
2 Qualifies the microfrontend capability, allows navigating to this microfrontend using the qualifier {entity: 'product'}.
3 Describes the capability.
4 Declares optional and required parameter(s) of this capability. Required parameters must be passed when navigating to this microfrontend. Parameters can be referenced in the path in the form of named parameters using the colon syntax (:).
5 Makes this a public microfrontend, allowing other micro applications to navigate to this microfrontend. By default, capabilities have application-private scope.
6 Section to associate metadata with a capability.
7 Metadata specific to the microfrontend capability, specifying the path to the microfrontend.

The path is relative to the application’s base URL, as specified in the application manifest. If the application does not declare a base URL, it is relative to the origin of the manifest file.
In the path, you can reference qualifier and parameter values in the form of named parameters. Named parameters begin with a colon (:) followed by the parameter or qualifier name, and are allowed in path segments, query parameters, matrix parameters and the fragment part. The router will substitute named parameters in the URL accordingly.

The microfrontend capability can also declare a preferred target outlet, as follows:

  {
    "type": "microfrontend",
    "qualifier": {
      "entity": "products"
    },
    "properties": {
      "path": "products",
      "outlet": "aside", (1)
    }
  }  
1 Specifies the preferred outlet to load this microfrontend into. Note that this preference is only a hint that will be ignored if the navigator specifies an outlet for navigation.

Note that the providing micro application does not need to install an intent handler for its microfrontend capabilities. The platform intercepts microfrontend intents and performs the navigation.

Outlet Resolution Rules

When navigating via intent, the target outlet is resolved as follows:

  • Outlet as specified by navigator via NavigationOptions#outlet.

  • Preferred outlet as specified in the microfrontend capability.

  • Current outlet if navigating in the context of an outlet.

  • primary outlet.

Persistent Navigation

Persistent navigation refers to the mechanism for restoring the navigational state after an application reload.

The router does not provide an implementation for persistent navigation out-of-the-box, mostly because many persistence strategies are imaginable. For example, the navigational state could be added to the top-level URL, stored in local storage, or persisted in the backend.

However, you can easily implement persistent navigation yourself. The router publishes navigations to the topic sci-router-outlets/:outlet/url; thus, they can be captured and persisted. When starting the application, you can then replay persisted navigations using the router.

For illustrative purposes, the following code snippet shows how to capture and persist navigations.

  Beans.get(MessageClient).observe$<string>('sci-router-outlets/:outlet/url')
    .subscribe(navigation => { (1)
      const topic = navigation.topic;
      const url = navigation.body;
      persistNavigation(topic, url); (2)
    });
1 Captures navigations by subscribing to the wildcard topic sci-router-outlets/:outlet/url.
2 Persists navigations, e.g., adds the navigations to the top-level URL.

When starting the application, you can replay persisted navigations as following.

  loadNavigations().forEach(([url, outlet]) => { (1)
    Beans.get(OutletRouter).navigate(url, {outlet: outlet}); (2)
  });
1 Loads persisted navigations, e.g., from the top-level URL.
2 Replays persisted navigations, instructing outlets to restore their content.
Unloading Outlet Content

To unload an outlet’s content, use null as the URL when routing, as follows:

  Beans.get(OutletRouter).navigate(null, {outlet: 'aside'});
Browsing History and Session History

Routing does not add an entry to the browsing history, and, by default, not push a navigational state to the browser’s session history stack.

You can instruct the router to add a navigational state to the browser’s session history stack, allowing the user to use the back button of the browser to navigate back in an outlet.

  Beans.get(OutletRouter).navigate('https://micro-frontends.org', {
    outlet: 'aside',
    pushStateToSessionHistoryStack: true, (1)
  });
1 Navigates pushing a new state to the browser’s session history stack.

3.5. Activator

This chapter covers the Activator API of the SCION Microfrontend Platform.


What is an Activator?

An activator allows a micro application to initialize and connect to the platform upon host application’s startup, i.e., when the user loads the web application into the browser. An activator is a startup hook for micro applications to initialize or register message or intent handlers. In the broadest sense, an activator is a kind of microfrontend, i.e. an HTML page that runs in an iframe. In contrast to regular microfrontends, however, at platform startup, the platform loads activator microfrontends into hidden iframes for the entire platform lifecycle, thus, providing a stateful session to the micro application on the client-side.

An activator is loaded into the browser exactly once, for the entire lifecycle of the application. Some typical use cases for activators include handling intents and messages, preloading data, or flexibly providing capabilities. If implementing single sign-on authentication, you could, for example, obtain the user’s access token and preemptively refresh it before its expected expiration.

A micro application registers an activator as public activator capability in its manifest, as follows:

"capabilities": [
  {
    "type": "activator", (1)
    "private": false, (2)
    "properties": {
      "path": "path/to/the/activator" (3)
    }
  }
]
1 Activators have the type activator.
2 Activators must have public visibility.
3 Path where the platform can load the activator microfrontend. The path is relative to the base URL of the micro application, as specified in the application manifest.

Activation Context

An activator’s microfrontend runs inside an activation context. The context provides access to the activator capability, allowing to read properties declared on the activator capability.

You can obtain the activation context using the ContextService as following.

    // Checks if running in an activation context.
    const isPresent = await Beans.get(ContextService).isPresent(ACTIVATION_CONTEXT);

    // Looks up the activation context.
    const ctx: ActivationContext = await Beans.get(ContextService).lookup(ACTIVATION_CONTEXT);

    // Reads properties declared on the activator capability.
    const properties = ctx.activator.properties;

Multiple Activators

A micro application can register multiple activators. Note, that each activator boots the micro application on its own and runs in a separate browsing context. The platform nominates one activator of each micro application as its primary activator. The nomination has no relevance to the platform but can help code decide whether or not to install singleton functionality.

You can test if running in the primary activation context as following.

    // Looks up the activation context.
    const ctx = await Beans.get(ContextService).lookup<ActivationContext>(ACTIVATION_CONTEXT);

    // Checks if running in the context of the primary activator.
    const isPrimary: boolean = ctx.primary;

Signaling Readiness

Starting an activator may take some time. In order not to miss any messages or intents, you can instruct the platform host to wait to enter started state until you signal the activator to be ready.

For this purpose, you can define a set of topics where to publish a ready message to signal readiness. If you specify multiple topics, the activator enters ready state after you have published a ready message to all these topics. A ready message is an event; thus, a message without payload.

If not specifying a readiness topic, the platform host does not wait for this activator to become ready. However, if you specify a readiness topic, make sure that your activator has a fast startup time and signals readiness as early as possible not to delay the startup of the platform host. Optionally, you can configure a maximum time that the host waits for an application to signal readiness. For more information, refer to chapter Configuring the Platform.

"capabilities": [
  {
    "type": "activator",
    "private": false,
    "properties": {
      "path": "path/to/the/activator",
      "readinessTopics": ["app/activator/ready"] (1)
    }
  }
]
1 Specifies one or more readiness topics. Activators of different applications can publish their readiness status on the same topic. The platform checks from which activator received readiness messages originate.

In the activator, when you are ready, you can signal your readiness as follows:

  Beans.get(MessageClient).publish('app/activator/ready');

Alternatively, you can read the configured readiness topic directly from the activator capability. However, this only works if you specify a single readiness topic.

    // Looks up the activation context.
    const activationContext = await Beans.get(ContextService).lookup<ActivationContext>(ACTIVATION_CONTEXT);

    // Read the configured readiness topic from the activator capability.
    Beans.get(MessageClient).publish(activationContext.activator.properties.readinessTopics as string);

Sharing State

Since an activator runs in a separate browsing context, microfrontends cannot directly access its state. Instead, an activator could put data, for example, into session storage, so that microfrontends of its micro application can access it. Alternatively, an activator could install a message listener, allowing microfrontends to request data via client-side messaging. For more information, refer to Cross Application Communication.

4. Configuration and Startup

This chapter describes how to configure and start the SCION Microfrontend Platform.


Starting the Platform in the Host Application

In the host application the SCION Microfrontend Platform is configured and web applications that want to interact with the platform are registered. For a detailed overview of platform and application configuration properties, refer to chapter Configuring the Platform.

The host application starts the platform by calling the MicrofrontendPlatformHost.start method.

  await MicrofrontendPlatformHost.start({
    applications: [ (1)
      {
        symbolicName: 'product-catalog-app',
        manifestUrl: 'https://product-catalog.webshop.io/manifest.json',
      },
      // ... some more micro applications
    ],
  });
1 Lists the applications allowed to interact with the platform.

The platform should be started during the bootstrapping of the host application. In Angular, for example, the platform is typically started in an app initializer. Since starting the platform host may take some time, you should wait for the startup Promise to resolve before interacting with the platform.

The host application can provide a manifest to declare intentions and contribute behavior to integrated applications via MicrofrontendPlatformConfig.host.manifest. The manifest can be specified either as an object literal or as a URL to load it over the network.

  await MicrofrontendPlatformHost.start({
    host: {
      manifest: { (1)
        name: 'Web Shop (Host)',
        capabilities: [
          // capabilities of the host application
        ],
        intentions: [
          // intentions of the host application
        ],
      },
    },
    applications: [ (2)
      {
        symbolicName: 'product-catalog-app',
        manifestUrl: 'https://product-catalog.webshop.io/manifest.json',
      },
      // ... some more micro applications
    ],
  });
1 Specifies the host manifest. Alternatively, you can pass a URL to the manifest for loading it over the network.
2 Lists the applications allowed to interact with the platform.

Connecting to the Platform from a Microfrontend

A microfrontend connects to the platform host by invoking the connect method on MicrofrontendPlatformClient and passing its application identity as the argument.

The following code snippet illustrates how to connect to the platform from a microfrontend.

  await MicrofrontendPlatformClient.connect('product-catalog-app');

A microfrontend should connect to the platform host during application bootstrapping. In Angular, for example, this is typically done in an app initializer. Since connecting to the platform host is an asynchronous operation, the microfrontend should wait for the Promise to resolve before interacting with the platform or other microfrontends.

The platform connects to the host through its window hierarchy. Therefore, the microfrontend must be embedded as direct or indirect child window of the host application window.

Configuring the Platform

You configure the platform by passing a config object when starting the platform host. Besides listing micro applications allowed to interact with the platform, you can specify the manifest of the host application, control platform behavior, declare common properties available to micro applications, and more.

Properties of MicrofrontendPlatformConfig
Property Type Mandatory Default Description

applications

yes

Lists the applications allowed to interact with the platform.

See ApplicationConfig for an overview of the properties.

host

no

Configures the interaction of the host application with the platform.

As with micro applications, you can provide a manifest for the host, allowing the host to contribute capabilities and declare intentions.

See HostConfig for an overview of the properties.

activatorApiDisabled

boolean

no

true

Controls whether the Activator API is enabled.

Activating the Activator API enables micro applications to contribute activator microfrontends. Activator microfrontends are loaded at platform startup for the entire lifecycle of the platform. An activator is a startup hook for micro applications to initialize or register message or intent handlers to provide functionality.

manifestLoadTimeout

number

no

Maximum time (in milliseconds) that the platform waits until the manifest of an application is loaded.

You can set a different timeout per application via ApplicationConfig.manifestLoadTimeout. If not set, by default, the browser’s HTTP fetch timeout applies. Consider setting this timeout if, for example, a web application firewall delays the responses of unavailable applications.

activatorLoadTimeout

number

no

Maximum time (in milliseconds) for each application to signal readiness.

If specified and activating an application takes longer, the host logs an error and continues startup. Has no effect for applications which provide no activator(s) or are not configured to signal readiness. You can set a different timeout per application via ApplicationConfig.activatorLoadTimeout.

By default, no timeout is set, meaning that if an app fails to signal readiness, e.g., due to an error, that app would block the host startup process indefinitely. It is therefore recommended to specify a timeout accordingly.

liveness

no

Configures the liveness probe performed between host and clients to detect and dispose stale clients.

See LivenessConfig for an overview of the properties.

properties

Dictionary

no

Defines user-defined properties which can be read by micro applications via PlatformPropertyService.

The ApplicationConfig object is used to describe a micro application to be registered as micro application. Registered micro applications can connect to the platform and interact with each other.

Properties of ApplicationConfig
Property Type Mandatory Default Description

symbolicName

string

yes

Unique symbolic name of this micro application.

The symbolic name must be unique and contain only lowercase alphanumeric characters and hyphens.

manifestUrl

string

yes

URL to the application manifest.

See Manifest for an overview of the properties of the manifest.

secondaryOrigin

string

no

Specifies an additional origin (in addition to the origin of the application) from which the application is allowed to connect to the platform.

By default, if not set, the application is allowed to connect from the origin of the manifest URL or the base URL as specified in the manifest file. Setting an additional origin may be necessary if, for example, integrating microfrontends into a rich client, enabling an integrator to bridge messages between clients and host across browser boundaries.

manifestLoadTimeout

number

no

Maximum time (in milliseconds) that the host waits until the manifest for this application is loaded.

If set, overrides the global timeout as configured in MicrofrontendPlatformConfig.manifestLoadTimeout.

activatorLoadTimeout

number

no

Maximum time (in milliseconds) for this application to signal readiness. If activating this application takes longer, the host logs an error and continues startup.

If set, overrides the global timeout as configured in MicrofrontendPlatformConfig.activatorLoadTimeout.

exclude

boolean

no

false

Excludes this micro application from registration, e.g. to not register it in a specific environment.

scopeCheckDisabled

boolean

no

false

Controls whether this micro application can interact with private capabilities of other micro applications.

By default, scope check is enabled. Disabling scope check is strongly discouraged.

intentionCheckDisabled

boolean

no

false

Controls whether this micro application can interact with the capabilities of other apps without having to declare respective intentions.

By default, intention check is enabled. Disabling intention check is strongly discouraged.

intentionRegisterApiDisabled

boolean

no

true

Controls whether this micro application can register and unregister intentions dynamically at runtime.

By default, this API is disabled. Enabling this API is strongly discouraged.

The HostConfig object is used to configure the interaction of the host application with the platform.

Properties of HostConfig
Property Type Mandatory Default Description

symbolicName

string

no

host

Symbolic name of the host.

If not set, host is used as the symbolic name of the host. The symbolic name must be unique and contain only lowercase alphanumeric characters and hyphens.

manifest

string | Manifest

no

The manifest of the host.

The manifest can be passed either as an object literal or specified as a URL to be loaded over the network. Providing a manifest lets the host contribute capabilities or declare intentions.

See Manifest for an overview of the properties of the manifest.

scopeCheckDisabled

boolean

no

false

Controls whether the host can interact with private capabilities of other micro applications.

By default, scope check is enabled. Disabling scope check is strongly discouraged.

intentionCheckDisabled

boolean

no

false

Controls whether the host can interact with the capabilities of other apps without having to declare respective intentions.

By default, intention check is enabled. Disabling intention check is strongly discouraged.

intentionRegisterApiDisabled

boolean

no

true

Controls whether the host can register and unregister intentions dynamically at runtime.

By default, this API is disabled. Enabling this API is strongly discouraged.

messageDeliveryTimeout

number

no

10'000ms

Maximum time (in milliseconds) that the platform waits to receive dispatch confirmation for messages sent by the host until rejecting the publishing Promise.

The LivenessConfig object is used to configure the liveness probe performed between host and clients to detect and dispose stale clients. Clients not replying to the probe are removed.

Properties of LivenessConfig
Property Type Mandatory Default Description

interval

number

yes

60s (if no LivenessConfig is provided)

Interval (in seconds) at which liveness probes are performed between host and connected clients.

Note that the interval must not be 0 and be greater than twice the timeout period to give a probe enough time to complete before performing a new probe.

timeout

number

yes

10s (if no LivenessConfig is provided)

Timeout (in seconds) after which a client is unregistered if not replying to the probe.

Note that the timeout must not be 0 and be less than half the interval period to give a probe enough time to complete before performing a new probe.

5. Miscellaneous

This part of the developer guide contains additional information that may be helpful when working with the SCION Microfrontend Platform.

5.1. Platform Lifecycle

This chapter describes the lifecycle of the SCION Microfrontend Platform.


5.1.1. Platform Lifecycle States

The lifecycle of the SCION Microfrontend Platform is controlled by the class MicrofrontendPlatform, providing methods to start and stop the platform. During its lifecycle, it traverses different lifecycle states, as defined in the enumeration PlatformState.

Starting

Indicates that the platform is about to start.

Started

Indicates that the platform started.

Stopping

Indicates that the platform is about to stop.

Stopped

Indicates that the platform is not running.

You can register a listener that is called when the platform enters a state, as follows:

  MicrofrontendPlatform.whenState(PlatformState.Starting).then(() => {
    // invoked when the platform is about to start.
  });

  MicrofrontendPlatform.whenState(PlatformState.Started).then(() => {
    // invoked after the platform is started
  });

  MicrofrontendPlatform.whenState(PlatformState.Stopping).then(() => {
    // invoked when the platform is about to stop.
  });

  MicrofrontendPlatform.whenState(PlatformState.Stopped).then(() => {
    // invoked when the platform is stopped.
  });

Alternatively, a bean can implement the PreDestroy lifecycle hook to get notified when the platform is about to stop, as follows:

  class Bean implements PreDestroy {

    public preDestroy(): void {
      // invoked when the platform is about to stop.
    }
  }

5.1.2. Platform Startup Runlevels

During the transition from the Starting to the Started platform state, the platform cycles through different runlevels for running initializers, enabling the controlled initialization of platform services. A runlevel is represented by a number greater than or equal to 0. Initializers can specify a runlevel in which to execute. Initializers bound to lower runlevels execute before initializers of higher runlevels. Initializers of the same runlevel may execute in parallel. The platform enters the state Started after all initializers have completed.

The platform defines the runlevels 0 to 3, as follows:

Runlevel 0

In runlevel 0, the platform host fetches manifests of registered micro applications.

Runlevel 1

In runlevel 1, the platform constructs eager beans.

Runlevel 2

From runlevel 2 and above, messaging is enabled. This is the default runlevel at which initializers execute if not specifying any runlevel.

Runlevel 3

In runlevel 3, the platform host installs activator microfrontends.

To hook into the platform’s startup process, you can register an initializer and bind it to a runlevel. You are not limited to the runlevels 0 to 3. For example, to run an initializer after all other initializers have completed, register the initializer in runlevel 4 or higher.

The following code snippet illustrates how to register an initializer in runlevel 4:

  Beans.registerInitializer({
    useFunction: async () => {
      // do some initialization in runlevel 4
    },
    runlevel: 4,
  });

5.1.3. Platform Startup Progress

Starting the host may take some time. During startup, the manifests of the registered applications are fetched, activator microfrontends are installed, and the platform waits until all applications have signaled readiness.

You can monitor the startup progress and provide feedback to the user like displaying a progress bar or a spinner. The platform reports the progress as a percentage number.

The following code snippet illustrates how to monitor the startup progress:

  // Invoke just before starting the SCION Microfrontend Platform.
  MicrofrontendPlatformHost.startupProgress$.subscribe((progress: number) => {
    // Update your progress indicator here.
    // The reported progress is a percentage number between `0` and `100`.
  });

The platform tracks the progress of following activities:

Loading manifests

Advances the progress after fetching the manifest of each registered application.

Loading activators

Advances the progress after loading the activator(s) of each application.

Running initializers

Advances the progress after the platform enters the Started state. Thus, initializers contribute to the overall progress.

5.1.4. Platform Shutdown

When the platform is stopped, it first enters the Stopping platform state. In this state, the beans registered with the platform are destroyed. Beans are destroyed according to their destruction order as specified at bean registration. Beans of the same destroy order are destroyed in reverse construction order. Beans can implement the PreDestroy lifecycle hook to get notified when they are about to be destroyed.

The platform allows the application to send messages while the platform is stopping. After all beans are destroyed, the client is disconnected from the host and the platform enters the Stopped state.

By default, the platform initiates shutdown when the browser unloads the document, i.e., when beforeunload is triggered. The main reason for beforeunload instead of unload is to avoid posting messages to disposed windows. However, if beforeunload is not triggered, e.g., when an iframe is removed, we fall back to unload.

You can change this behavior by registering a custom MicrofrontendPlatformStopper bean, as follows:

  class OnUnloadMicrofrontendPlatformStopper implements MicrofrontendPlatformStopper {

    constructor() {
      // Destroys the platform when the document is about to be unloaded.
      window.addEventListener('unload', () => MicrofrontendPlatform.destroy(), {once: true});
    }
  }

  // Registers custom platform stopper.
  Beans.register(MicrofrontendPlatformStopper, {useClass: OnUnloadMicrofrontendPlatformStopper});

5.2. Intercepting Host Manifest

When starting the platform in the host application, the host can pass a manifest to express its intentions and provide functionality in the form of capabilities. If integrating the platform in a library, you may need to intercept the manifest of the host in order to introduce library-specific behavior. For this reason, the platform provides a hook to intercept the manifest of the host before it is registered with the platform.

You can register a host manifest interceptor in the bean manager, as following:

  Beans.register(HostManifestInterceptor, {useClass: YourInterceptor, multi: true});

The following interceptors adds an intention and capability to the host manifest.

  class YourInterceptor implements HostManifestInterceptor {

    public intercept(hostManifest: Manifest): void {
      hostManifest.intentions = [
        ...hostManifest.intentions || [],
        provideMicrofrontendIntention(), (1)
      ];
      hostManifest.capabilities = [
        ...hostManifest.capabilities || [],
        provideMessageBoxCapability(), (2)
      ];
    }
  }

  function provideMicrofrontendIntention(): Intention {
    return {
      type: 'microfrontend',
      qualifier: {'*': '*'},
    };
  }

  function provideMessageBoxCapability(): Capability {
    return {
      type: 'messagebox',
      qualifier: {},
      private: false,
      description: 'Allows displaying a simple message to the user.',
    };
  }
1 Registers an intention in the host manifest, e.g., an intention to open microfrontends.
2 Registers a capability in the host manifest, e.g., a capability for displaying a message box.

5.3. Focus

By design of iframe isolation, DOM events, including focusin and focusout, do not bubble across iframe boundaries. SCION helps overcome this iframe restriction and monitors microfrontends for focus gain or loss, allowing to implement use cases where an overlay should close upon focus loss.

Note that SCION can only monitor the focus of microfrontends that are connected to the platform.

5.3.1. Focus Monitor

The focus monitor can be used to observe whether the current microfrontend has received focus or contains embedded web content that has received focus.

  • FocusMonitor.focus$
    Informs when the current microfrontend has gained or lost focus.

      Beans.get(FocusMonitor).focus$.subscribe(hasFocus => {
        console.log('on focus change', hasFocus);
      });
  • FocusMonitor.focusWithin$
    Informs when the current microfrontend or any of its child microfrontends has gained or lost focus. It behaves like the :focus-within CSS pseudo-class but operates across iframe boundaries. For example, it can be useful when implementing overlays that close upon focus loss. The Observable does not re-emit while the focus remains within this microfrontend or any of its child microfrontends.

      Beans.get(FocusMonitor).focusWithin$.subscribe(isFocusWithin => {
        console.log('on focus change', isFocusWithin);
      });

5.3.2. Focus DOM Event

The router outlet <sci-router-outlet> fires a focuswithin custom DOM event when the microfrontend loaded into the outlet, or any of its child microfrontends, has gained or lost focus. It contains the current focus-within state in its details property as a boolean value: true if focus was gained, or false if focus was lost.

  <sci-router-outlet onfocuswithin="onFocusWithin()"></sci-router-outlet>

For an Angular application, it would look as follows:

  <sci-router-outlet (focuswithin)="onFocusWithin($event)"></sci-router-outlet>

The event does not bubble up through the DOM. After gaining focus, the event is not triggered again until embedded content loses focus completely, i.e., when focus does not remain in the embedded content at any nesting level. This event behaves like the :focus-within CSS pseudo-class but operates across iframe boundaries. For example, it can be useful when implementing overlays that close upon focus loss.

5.4. Routing in Micro Applications

The platform recommends using hash-based routing over HTML 5 push-state routing in micro applications.

When routing between microfrontends, the router outlet sets the iframe location to the URL of the routed microfrontend. When navigating between microfrontends of the same micro application, push-state routing would cause the micro-application to reload on every routing. As hash-based routing uses the fragment part (#) of the URL to simulate different routes, routing does not cause the user agent to load the application anew, resulting in better user experience.

Refer to the documentation of the UI framework of the micro application on how to activate hash-based routing.

If you use hash-based routing, do not forget to set the base URL of the application to # in the manifest, as follows:

{
  "name": "Your App",
  "baseUrl": "#"
}

5.5. Bean Manager

The bean manager allows you to get references to platform services to interact with the SCION Microfrontend Platform. For example, you can obtain the message client for publishing messages as follows:

import { MessageClient } from '@scion/microfrontend-platform';
import { Beans } from '@scion/toolkit/bean-manager';

const messageClient = Beans.get(MessageClient);

The bean manager is part of the @scion/toolkit module. For more information about its usage, refer to the documentation of the bean manager.

5.6. Semantic Versioning Scheme

SCION Microfrontend Platform follows the semantic versioning scheme (SemVer) for its releases. In this scheme, a release is represented by three numbers: MAJOR.MINOR.PATCH. For example, version 1.5.3 indicates major version 1, minor version 5, and patch level 3.

Major Version

The major version number is incremented when introducing any backwards incompatible changes to the API.

Minor Version

The minor version number is incremented when introducting some new, backwards compatible functionality.

Patch Level

The patch or maintenance level is incremented when fixing bugs.

In the development of a new major release, we usually release pre-releases and tag them with the beta tag (-beta.x). A beta pre-release is a snapshot of current development, so it is potentially unstable and incomplete. Before releasing the major version, we start releasing one or more release candidates, which we tag with the rc tag (-rc.x). We will publish the official and stable major release if the platform is working as expected and we do not find any critical problems.

semver

5.7. Compatibility and Deprecation Policy

We are aware that you need stability from the SCION Microfrontend Platform, primarily because microfrontends with potential different lifecycles are involved. Therefore, you can expect a decent release cycle of one or two major releases per year with strict semantic versioning policy. Changes to the communication protocol between the host and micro applications are backward compatible in the same major release. This allows for the host and its clients to update independently.

Deprecation of APIs can occur in any release. Deprecated APIs are only removed in a major release. :basedir: ../../.. :terminologydir: ../../../chapters/terminology

5.8. ECMAScript Transpilation Level

We recommend using ES2015 transpilation level or higher to have native web component support because the SCION Microfrontend Platform uses a web component for embedding microfrontends. The web component wraps an iframe and loads, based on the current router state, the routed microfrontend into the iframe.

By default, native web component browser support is only available for code transpiled to ES2015 or higher, mainly because the browser expects a web component to be a native ES2015 class. In ES5, ES2015 classes are represented as functions, which results in a runtime error when using web components.

If you have to transpile your app to ES5 compliant code and still want to use the browser’s native web component support, we recommend importing the adapter custom-elements-es5-adapter.js. This adapter converts ES5 style classes back into native ES2015 classes. Alternatively, you could also use a polyfill to simulate native web component support.

Since version 1.0.0-beta.5 we no longer include the distributions for esm5 and fesm5 in the @scion/microfrontend-platform’s NPM package. Only the formats for esm2015, fesm2015, and UMD are distributed. Consequently, the module field in package.json points to the fesm2015 distribution.

If requiring esm5 or fesm5, you will need to downlevel to ES5 yourself. If using Angular, the Angular CLI will automatically downlevel the code to ES5 if differential loading is enabled in the Angular project, so no action is required from Angular CLI users.

{
  ...
  "main": "bundles/scion-microfrontend-platform.umd.js",
  "module": "fesm2015/scion-microfrontend-platform.js",
  "es2015": "fesm2015/scion-microfrontend-platform.js",
  "esm2015": "esm2015/scion-microfrontend-platform.js",
  "fesm2015": "fesm2015/scion-microfrontend-platform.js",
  ...
}

5.9. Explore SCION Microfrontend Platform

To explore the platform and experiment with its features, you can play around with our technical demo app, available at the following URL: https://scion-microfrontend-platform-testing-app1-v1-2-2.vercel.app.

We use this application internally to run our end-to-end test-suite against it, so the focus is not on an attractive user interface, responsive design, or easy navigation. The application is a composition of browser outlets to display multiple microfrontends simultaneously. A browser outlet is like an internal browser in the application, implemented as a named router-outlet. If you enter a URL in the address bar of a browser outlet, internal routing takes place to display the requested web content.


Displaying a Web App as Microfrontend

When you open the application in your browser, the application displays two browser outlets. If you enter a URL in the address bar of a browser outlet, the application loads the page as a microfrontend.

e2e testing app
The web application must support embedding in an iframe, i.e., the page must not have the HTTP header X-Frame-Options set. Otherwise, the browser will refuse the embedding.

Discovering Platform Features

Various platform features are available as microfrontends, which you can open from the drop-down list in the address bar of a browser outlet. The demo app is deployed multiple times under different origins, allowing for testing of platform features across origin boundaries.

e2e testing app microfrontend chooser

Messaging in Action

The demo app provides a microfrontend for receiving messages and another microfrontend for publishing messages. To experience messaging in action, in the left browser outlet, enter receive in the address bar and select the microfrontend of the desired origin. Then, do the same in the right browser outlet, but enter publish instead.

You can now subscribe to messages published on a topic. In the left browser outlet, choose Topic as the flavor, and enter the topic. Then, click on the Subscribe button. In the right browser outlet, you can publish messages. Select Topic as the flavor, enter a topic name and a message. Then, click on the Publish button.

The example below subscribes to the topic myhome/:room/temperature using :room as a wildcard path segment, thus receives the temperatures of any room.

e2e testing app pub sub example

Router-Outlet in Action

The demo app provides a microfrontend for embedding web content in a named router-outlet. To experience routing in action, in the left browser outlet, enter router-outlet in the address bar and select the microfrontend of the desired origin. Then, do the same in the right browser outlet, but enter outlet-router instead.

The left browser outlet should now show a blank router-outlet. Enter a name for the router-outlet, e.g., test, then click on the Apply button. The router-outlet stays blank because no routing took place yet for that router-outlet. In the right browser outlet, enter the name of the target router-outlet, which in our example is test, and enter the URL of the microfrontend to display in the target router-outlet, e.g., https://micro-frontends.org. Then, click on the Navigate button. The microfrontend should now display in the router-outlet on the left.

e2e testing app router outlet example

Opening Browser Outlets

The demo application is not limited to two adjacent browser outlets. By changing the matrix parameter count in the browser URL, you can control how many browser outlets to display at the top level.

e2e testing app browser outlets example

You can also nest browser outlets by loading the browser-outlets microfrontend and playing around with its count matrix parameter.

e2e testing app nested browser outlets example

5.10. Angular Integration Guide

This part of the documentation is for developers who want to integrate the SCION Microfrontend Platform in an Angular application.


Starting the Platform Host in an Angular App Initializer

The platform should be started during the bootstrapping of the Angular application, that is, before displaying content to the user. Hence, we recommend starting the platform in an app initializer. See chapter Using a Route Resolver instead of an App Initializer for an alternative approach.

Angular allows hooking into the process of initialization by providing an initializer to Angular’s APP_INITIALIZER injection token. Angular will wait until all initializers resolved to start the application, making it the ideal place for starting the SCION Microfrontend Platform.
We recommend starting the platform outside the Angular zone in order to avoid excessive change detection cycles of platform-internal subscriptions to global DOM events.

The following listing illustrates how to configure an Angular APP_INITIALIZER to start the SCION Microfrontend Platform in the host application. Note that APP_INITIALIZER expects a higher order factory function to be specified.

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(appRoutes),
    provideMicrofrontendPlatformHost(), (1)
  ],
});

function provideMicrofrontendPlatformHost(): EnvironmentProviders {
  return makeEnvironmentProviders([
    {
      provide: APP_INITIALIZER,
      useFactory: startHostFn, (2)
      multi: true,
    },
  ]);
}

/**
 * Starts the SCION Microfrontend Platform in the host application.
 */
function startHostFn(): () => Promise<void> {
  const zone = inject(NgZone); (3)
  return (): Promise<void> => {
    Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)}); (4)
    const config: MicrofrontendPlatformConfig = {applications: [...]}; (5)
    return zone.runOutsideAngular(() => MicrofrontendPlatformHost.start(config)); (6)
  };
}
1 Registers a set of DI providers to set up the SCION Microfrontend Platform in the host application.
2 Instruments Angular to start the SCION Microfrontend Platform during application startup.
3 Injects NgZone using Angular’s inject function.
4 Registers a decorator to synchronize platform-specific RxJS Observables with the Angular Zone. See Synchronizing RxJS Observables with the Angular Zone for more information.
5 Constructs the config object to configure the platform and list applications that are allowed to interact with the platform. Refer to Configuring the Platform for more information about configuring the platform.
6 Starts the platform host outside the Angular zone.
Refer to chapter Starting the Platform in the Host Application for detailed instructions on how to start the platform in the host.

Connecting to the Host in an Angular App Initializer

A micro application should connect to the platform host during the bootstrapping of the Angular application, that is, before displaying content to the user. Hence, we recommend connecting to the platform host in an app initializer. See chapter Using a Route Resolver instead of an App Initializer for an alternative approach.

Angular allows hooking into the process of initialization by providing an initializer to Angular’s APP_INITIALIZER injection token. Angular will wait until all initializers resolved to start the application, making it the ideal place for starting the SCION Microfrontend Platform.
We recommend connecting to the platform host outside the Angular zone in order to avoid excessive change detection cycles of platform-internal subscriptions to global DOM events.

The following listing illustrates how to configure an Angular APP_INITIALIZER to connect to the platform from a micro application.

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(appRoutes, withHashLocation()),
    provideMicrofrontendPlatformClient(), (1)
  ],
});

function provideMicrofrontendPlatformClient(): EnvironmentProviders {
  return makeEnvironmentProviders([
    {
      provide: APP_INITIALIZER,
      useFactory: connectToHostFn, (2)
      multi: true,
    },
  ]);
}

/**
 * Connects to the host application.
 */
function connectToHostFn(): () => Promise<void> {
  const zone = inject(NgZone); (3)
  return (): Promise<void> => {
    Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)}); (4)
    return zone.runOutsideAngular(() => MicrofrontendPlatformClient.connect('APP_SYMBOLIC_NAME')); (5)
  };
}
1 Registers a set of DI providers to set up the SCION Microfrontend Platform in a client application.
2 Instruments Angular to connect to the platform host during application startup.
3 Injects NgZone using Angular’s inject function.
4 Registers a decorator to synchronize platform-specific RxJS Observables with the Angular Zone. See Synchronizing RxJS Observables with the Angular Zone for more information.
5 Connects to the platform host outside the Angular zone, passing the symbolic name of the application as argument.
Refer to chapter Connecting to the Platform from a Microfrontend for detailed instructions on how to connect to the host from a microfrontend.

Using a Route Resolver instead of an App Initializer

If you cannot use an app initializer for starting the platform or connecting to the platform host, an alternative would be to use a route resolver. Resolvers can be used to resolve data or run code prior to route activation.

The following listing illustrates how to configure an Angular resolver to connect to the platform from a client application. Similarly, you could start the platform in the host application.

const appRoutes: Routes = [
  // Declare a component-less, empty-path route that uses a resolver to connect to the host.
  // Child routes are not loaded until connected to the host.
  {
    path: '',
    resolve: { (1)
      platform: () => {
        const zone = inject(NgZone); (2)
        Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)}); (3)
        return zone.runOutsideAngular(() => MicrofrontendPlatformClient.connect('APP_SYMBOLIC_NAME')); (4)
      },
    },
    children: [ (5)
      {
        path: 'microfrontend-1',
        loadComponent: () => import('./microfrontend-1/microfrontend-1.component'),
      },
      {
        path: 'microfrontend-2',
        loadComponent: () => import('./microfrontend-2/microfrontend-2.component'),
      },
    ],
  },
];

// Register the routes in the router.
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(appRoutes, withHashLocation()),
  ],
});
1 Installs a resolver on a component-less, empty-path route, which is parent to the microfrontend routes.
2 Injects NgZone using Angular’s inject function.
3 Registers a decorator to synchronize platform-specific RxJS Observables with the Angular Zone. See Synchronizing RxJS Observables with the Angular Zone for more information.
4 Connects to the platform host outside the Angular zone, passing the symbolic name of the application as argument.
5 Registers microfrontend routes as child routes.

Ensure that a micro application instance connects to the host only once. Therefore, it is recommended to install the resolver in a parent route common to all microfrontend routes. When loading a microfrontend for the first time, Angular will wait activating the child route until the platform finished starting. When navigating to another microfrontend of the micro application, the resolver does not resolve anew.

Configuring Hash-Based Routing

For client applications, we recommend enabling hash-based routing for the reasons explained in Routing in Micro Applications. For the host application, either strategy is fine.

To enable hash-based routing, pass the withHashLocation() feature to the router, as follows:

provideRouter(appRoutes, withHashLocation());

If you use hash-based routing, do not forget to set the base URL of the application to # in the manifest, as follows:

{
  "name": "Your App",
  "baseUrl": "#"
}

Instruct Angular to allow Custom Elements in Templates

The <sci-router-outlet> element for embedding microfrontends is a custom element according to the web component standard. For use in an Angular component, we need to tell the Angular compiler that we are using non-Angular elements. Otherwise, the compiler would complain that <sci-router-outlet> cannot be resolved to an Angular component.

For standalone components, set the CUSTOM_ELEMENTS_SCHEMA schema in the component metadata as follows:

@Component({
  selector: '...',
  // content skipped ...
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA], // required because <sci-router-outlet> is a custom element
})
export class YourComponent {
}

The schemas property is only available for standalone components. For components declared in an NgModule, set the schema in the module metadata instead.

Providing an Activator

Read chapter Activator to learn more about activators.

In order to install an activator, you need to register an activator capability in your manifest.

"capabilities": [
  {
    "type": "activator", (1)
    "private": false, (2)
    "properties": {
      "path": "activator" (3)
    }
  }
]
1 Activators have the type activator.
2 Activators must have public visibility.
3 Path where the platform can load the activator microfrontend. The path is relative to the base URL of the micro application, as specified in the application manifest.

We recommend creating a component-less, lazy-loaded activator module and registering it under the activator path as specified in the manifest.

const appRoutes: Routes = [
  {
    path: 'activator',
    loadChildren: () => import('./activator/activator.module'),
  },
];

The module will be instantiated when the platform loads registered activators. In the module constructor, you can perform initialization tasks such as installing message handlers.

@NgModule({}) (1)
export class ActivatorModule {

  constructor() { (2)
    // Perform initialization tasks such as installing message handlers.
  }
}
1 Declares the activator NgModule. No further metadata is required.
2 Performs initialization tasks such as installing message handlers.

It turned out that when using a route resolver to start the platform, Angular constructs the activator module immediately, that is, before the resolver completes. For this reason, if you start the platform inside a route resolver, and only then, you must wait for the platform to complete startup before interacting with the platform, as follows:

  constructor() {
    MicrofrontendPlatform.whenState(PlatformState.Started).then(() => {
      // Perform initialization tasks such as installing message handlers.
    });
  }

Providing Platform Beans for Dependency Injection

Beans of the SCION Microfrontend Platform can be provided for dependency injection, allowing objects managed by Angular to inject platform beans.

bootstrapApplication(AppComponent, {
  providers: [
    provideMicrofrontendPlatformBeans(),
    // other providers skipped ...
  ],
});

/**
 * Provides beans of @scion/microfrontend-platform for DI.
 */
function provideMicrofrontendPlatformBeans(): EnvironmentProviders {
  return makeEnvironmentProviders([
    {provide: MessageClient, useFactory: () => Beans.get(MessageClient)},
    {provide: IntentClient, useFactory: () => Beans.get(IntentClient)},
    {provide: OutletRouter, useFactory: () => Beans.get(OutletRouter)},
    {provide: ManifestService, useFactory: () => Beans.get(ManifestService)},
    {provide: ContextService, useFactory: () => Beans.get(ContextService)},
    {provide: PreferredSizeService, useFactory: () => Beans.get(PreferredSizeService)},
    {provide: PlatformPropertyService, useFactory: () => Beans.get(PlatformPropertyService)},
  ]);
}

Providing Microfrontends as Standalone Lazy-Loaded Components

Microfrontends should have a fast startup time and therefore only load code and data that they need. Therefore, we recommend providing microfrontends as standalone, lazy-loaded components.

const appRoutes: Routes = [
  {
    path: 'activator',
    loadChildren: () => import('./activator/activator.module'), (1)
  },
  {
    path: 'microfrontend-1',
    loadComponent: () => import('./microfrontend-1/microfrontend-1.component'), (2)
  },
  {
    path: 'microfrontend-2',
    loadComponent: () => import('./microfrontend-2/microfrontend-2.component'), (3)
  },
];
1 Registers the lazy-loaded route for the activator microfrontend.
2 Registers the lazy-loaded route for the microfrontend-1 microfrontend.
3 Registers the lazy-loaded route for the microfrontend-2 microfrontend.
4 Registers the declared routes with Angular.

Synchronizing RxJS Observables with the Angular Zone

Angular applications expect an RxJS Observable to emit in the same Angular zone in which subscribed to the Observable. That is, if subscribing inside the Angular zone, emissions are expected to be received inside the Angular zone. Otherwise, the UI may not be updated as expected but delayed until the next change detection cycle. Similarly, if subscribing outside the Angular zone, emissions are expected to be received outside the Angular zone. Otherwise, this would cause unnecessary change detection cycles resulting in potential performance degradation.

Therefore, the platform supports the decoration of its observables to emit in the correct zone. We strongly recommend decoration for Angular applications. A decorator must implement ObservableDecorator and be registered in the bean manager before the platform is started, as follows:

const zone: NgZone = inject(NgZone); (1)
Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)}); (2)
1 Injects the Angular NgZone.
2 Registers the decorator.

Example of a decorator for synchronizing the Angular zone.

/**
 * Mirrors the source, but ensures subscription and emission {@link NgZone} to be identical.
 */
export class NgZoneObservableDecorator implements ObservableDecorator {

  constructor(private zone: NgZone) {
  }

  public decorate$<T>(source$: Observable<T>): Observable<T> {
    return new Observable<T>(observer => {
      const insideAngular = NgZone.isInAngularZone(); (1)
      const subscription = source$
        .pipe(
          subscribeInside(fn => this.zone.runOutsideAngular(fn)), (2)
          observeInside(fn => insideAngular ? this.zone.run(fn) : this.zone.runOutsideAngular(fn)), (3)
        )
        .subscribe(observer);
      return () => subscription.unsubscribe();
    });
  }
}
1 Captures the zone at the time of subscription to the Observable.
2 Subscribes to the source outside the Angular zone.
3 Runs subscription handlers (next, error, complete) in the same zone in which subscribed to the Observable.

5.11. SCION DevTools

The SCION DevTools is a microfrontend that allows inspecting installed micro applications, their intentions and capabilities, and shows dependencies between applications. You can integrate the SCION DevTools microfrontend in your application as follows:

1. Register the SCION DevTools as micro application
  MicrofrontendPlatformHost.start({
    applications: [
      // register your micro application(s) here

      // register the 'devtools' micro application
      {
        symbolicName: 'devtools',
        manifestUrl: 'https://scion-microfrontend-platform-devtools-<version>.vercel.app/assets/manifest.json',
        intentionCheckDisabled: true,
        scopeCheckDisabled: true,
      },
    ],
  });
Note that we need to disable some checks for the SCION DevTools to have access to private capabilities. We strongly recommend not to do this for regular micro applications.

With each release of the SCION Microfrontend Platform, we also publish a new version of the SCION DevTools. We strongly recommend integrating the SCION DevTools via the versioned URL to be compatible with your platform version.

2. Create a router outlet to display the SCION DevTools
  <sci-router-outlet name="devtools"></sci-router-outlet>
3. Load SCION DevTools into the router outlet

Using the OutletRouter you can navigate to the SCION DevTools microfrontend. You can either navigate via URL or via intent. We recommend using intent-based routing to make navigation flows explicit.

  • Integrate SCION DevTools via Intent

    The SCION DevTools provide a microfrontend capability with the following qualifier: {component: 'devtools', vendor: 'scion'}.

    First, you need to declare an intention in your manifest, as follows:

    {
      "type": "microfrontend",
      "qualifier": {
        "component": "devtools",
        "vendor": "scion"
      }
    }

    Then you can route as following:

      Beans.get(OutletRouter).navigate({component: 'devtools', vendor: 'scion'}, {outlet: 'devtools'});
  • Integrate SCION DevTools via URL

    Pass the URL to the router. Do not forget to replace the version with your actual SCION Microfrontend Platform version, e.g., v1-2-2.

      Beans.get(OutletRouter).navigate('https://scion-microfrontend-platform-devtools-<version>.vercel.app', {outlet: 'devtools'});

SCION DevTools is available with a light and dark color scheme. By default, the user’s preferred OS color scheme is used. To customize, set the color-scheme context value on the router outlet into which loaded the SCION DevTools. Supported color schemes are light and dark.

  const devToolsRouterOutlet: SciRouterOutletElement = document.querySelector('sci-router-outlet'); (1)
  devToolsRouterOutlet.setContextValue('color-scheme', 'dark'); (2)
1 Get a reference to the router outlet into which loaded the SCION DevTools
2 Set the color scheme; can be light or dark.

SCION DevTools supports the display of a splash while loading. If navigating to the DevTools via intent, the splash is displayed by default. If navigating via URL the navigation option showSplash must be set. See chapter Splash for more information about displaying a splash.

6. Security

The platform follows the advice for secure communication regarding the use of the native postMessage mechanism, as described here: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Security_concerns

The platform permits only registered micro applications to connect to the platform. The host application registers qualified micro applications via a manifest file and assigns a unique identity to each micro application when starting the platform host. A micro application must pass this identity when connecting to the platform host. If the origin of the connecting micro application does not match that identity’s manifest origin, the platform rejects the connection attempt. This check prevents a micro application from pretending to be another micro application.

Internally, the platform dispatches messages through a message broker in the platform host. Each connected micro application knows the message broker Window, and the broker knows the Windows of connected micro applications. Therefore, a connected micro application can post messages directly to the broker’s Window, and the broker can then dispatch the message directly to the Window of the receiving application(s). As additional security, a message sender always specifies the target origin, and a message recipient always checks the origin of the sender. The only exception is when establishing a connection to the platform host or when constructing the context tree.

7. Terminology

Activator

An activator is a startup hook for micro applications to initialize or register message or intent handlers to provide functionality. Activator microfrontends are loaded at platform startup for the entire lifecycle of the platform

In the broadest sense, an activator is a kind of microfrontend, i.e. an HTML page that runs in an iframe. In contrast to regular microfrontends, however, at platform startup, the platform loads activator microfrontends into hidden iframes for the entire platform lifecycle, thus, providing a stateful session to the micro application on the client-side.

Application

This term is used throughout this guide to refer to the entire application as presented to the user.

Bean Manager

The bean manager is the central point to get references to platform services, such as the MessageClient, IntentClient or OutletRouter. The bean manager is provided by @scion/toolkit NPM package.

Capability

The term capability refers to the Intention API of the SCION Microfrontend Platform.

A capability represents some functionality of a micro application that is available to qualified micro applications through the Intention API. A micro application declares its capabilities in its manifest. Qualified micro applications can browse capabilities similar to a catalog, or interact with capabilities via intent.

A capability is formulated in an abstract way consisting of a type and optionally a qualifier. The type categorizes a capability in terms of its functional semantics (e.g., microfrontend if providing a microfrontend). Multiple capabilities can be of the same type. In addition to the type, a capability can define a qualifier to differentiate the different capabilities of the same type.

A capability can have private or public visibility. If private, the capability is not visible to other micro applications.

A capability can specify parameters which the intent issuer can/must pass along with the intent. Parameters are part of the contract between the intent publisher and the capability provider. They do not affect the intent routing, unlike the qualifier.

Cross-Origin Messaging

Cross-origin messaging means the communication between web content loaded from different origins.

Host Application

The host application, sometimes also called the container application, provides the top-level integration container for microfrontends. Typically, it is the web app which the user loads into the browser that provides the main application shell, defining areas to embed microfrontends.

The host app starts the platform host and registers micro applications. As with micro applications, the host can provide a manifest to interact with other micro applications and contribute behavior in the form of capabilities.

Intent

The term intent refers to the Intention API of the SCION Microfrontend Platform.

The intent is the message that a micro application sends to interact with functionality that is available in the form of a capability.

The platform transports the intent to the micro applications that provide the requested capability. A micro application can issue an intent only if having declared an intention in its manifest. Otherwise, the platform rejects the intent.

An intent is formulated in an abstract way, having assigned a type, and optionally a qualifier. This information is used for resolving the capability; thus, it can be thought of as a form of capability addressing. See the definition of a capability for more information.

Intention

The term intention refers to the Intention API of the SCION Microfrontend Platform.

An intention refers to one or more capabilities that a micro application wants to interact with. Manifesting intentions enables us to see dependencies between applications down to the functional level.

Intentions are declared in the application’s manifest and are formulated in an abstract way, consisting of a type and optionally a qualifier. The qualifier is used to differentiate capabilities of the same type.

Intention API

The Intention API enables controlled collaboration between micro applications. It is inspired by the Android platform where an application can start an activity via an Intent (such as sending an email).

To collaborate, an application must express an intention. An intention refers to one or more capabilities, or activity, in the Android platform. Capabilities can be browsed similar to a catalog and invoked by issuing an intent. Manifesting intentions enables us to see dependencies between applications down to the functional level.

Manifest

The manifest is a special file that contains information about a micro application.

A micro application declares its intentions and capabilities in its manifest file. The manifest needs to be registered in the host application.

Micro Application

A micro application deals with well-defined business functionality. It is a regular web application that provides one or more microfrontends. A micro application can communicate with other micro applications safely using the platform's cross-origin messaging API. A micro application has to provide an application manifest which to register in the host application.

Microfrontend

A microfrontend is a term of the microfrontend architecture design approach to developing frontend applications as a composition of small, self-contained components, so-called microfrontends. Each microfrontend focuses on a single business functionality, breaking up hard-to-handle monoliths into parts by allowing independent development, autonomous lifecycles, true code splitting, and the use of different stacks. Microfrontends should be as independent and isolated as possible so that a change in one microfrontend has no impact on other microfrontends.

Origin

The origin is defined by the scheme (protocol), host (domain), and port. Two objects have the same origin only when the scheme, host, and port all match.

Platform

SCION Microfrontend Platform is a TypeScript-based open source library that enables the implementation of a framework-agnostic microfrontend architecture using iframes.

The platform provides fundamental building blocks for implementing a microfrontend architecture. These include cross-microfrontend communication, embedding of microfrontends, and routing between microfrontends. SCION Microfrontend Platform is a lightweight, web stack agnostic library but not a framework. It has no user-facing components, and does not dictate any form of application structure.

Refer to chapter SCION Microfrontend Platform for more information about the platform.

Publish/Subscribe

The publish/subscribe pattern (also known as pub/sub) decouples the client that sends a message (the publisher) from the client or clients that receive the message (the subscribers). Clients do not know about each other. A broker dispatches the messages to interested (subscribed) clients.

Qualifier

The qualifier is a dictionary of arbitrary key-value pairs to differentiate capabilities of the same type.

To better understand the concept of the qualifier, a bean manager can be used as an analogy. If there is more than one bean of the same type, a qualifier can be used to control which bean to inject.

Router

Allows controlling the content displayed in a router outlet. Routing works across microfrontend and micro application boundaries, allowing the URL of an outlet to be set from anywhere in the application.

Router Outlet

Web component for embedding web content using the router. The outlet uses an iframe to achieve the highest possible level of isolation via a separate browsing context. The URL is set indirectly via the router, allowing to control the outlet content from anywhere in the application.

Same-Origin Policy

The same-origin policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin.