Skip to content
Andreas Ernst edited this page Mar 3, 2026 · 26 revisions

Introduction

Portal implements a react portal framework supporting microfrontends vastly simplifying implementation efforts since a number of technical challenges every application has to solve are already part of the framework.

  • integration of a DI solution
  • centralized error handling ( including error boundaries )
  • session handling
  • optional i18n solution
  • meta-data based approach that allows for
    • filtering of available features according to authentication, authorization or other aspects ( e.g. feature flags )
    • automatic router configuration according to the metadata
    • dynamic navigation features that are based on the meta-data and custom rules
    • feature outlets that cover both local and federated components and allow for custom async preloading logic ( e.g. i18n loading )
  • custom application configurations with support for both client and server side logic

While the framework supports enterprise portals with dynamic microfrontends - and server side configuration mechanisms - as one extreme it also covers small local only applications without significant coding and rampup overhead, making it a one-size-fits-all framework.

Basic Design

The main idea for most of the mechanisms is that modules expose meta-data of "what is inside", by annotating available "features" ( named components used internally or part of the routing ) with special decorators.

@Feature({
  id: 'public-navigation',
  label: 'Navigation',
  visibility: ["public"], // visible without a session
  features: [],
  permissions: []
  tags: ['portal'], // needed to identify this special feature
  path: '/'
})
export class PublicNavigationFeature extends React.Component {
    ...
}

A parser - as part of the build - will locate those features and generate a manifest.json.

{
  "id": "shell",
  "label": "Shell",
  "version": "1.0.0",
  "moduleName": "ApplicationModule",
  "sourceFile": "apps/shell/src/main.tsx",
  "description": "Shell",
  "features": [
     {
      "id": "public-navigation",
      "label": "Navigation",
      "path": "",
      "visibility": [
        "public"
      ],
      "tags": [
        "portal"
      ],
      "features": [
      ],
      "permissions": [
      ],
      "component": "PublicNavigationFeature",
      "sourceFile": "apps/app/src/navigation/PublicNavigation.tsx",
      "children": []
    },
    ...
  ],
  ...
}

If we think of a setup including a shell and federated microfrontends, those manifests are the basis for an application configuration by merging different manifests according to custom rules and setups and booting the application with a tailored configuration. Possible rules - that are typically executed on the server side - are:

  • filter microfrontends on a static basis
  • filter microfrontends by authorization aspects
  • filter features by
    • regarding roles and permissions
    • regarding feature flags
    • regarding client characteristics ( e.g. screen resolution )

Lets' look at the details

Module Setup

Let's see how to boot an application. First thing we need to do is to setup the di container and add a couple of instances inside of the main "module"

@Module({
  id: 'shell',
  label: 'Shell',
  version: '1.0.0',
  description: 'Shell',
  name: '',
})
export class ApplicationModule extends AbstractModule {
  @create()
  createSessionManager() : SessionManager<any,any> {
    return new SessionManager(new DummyAuthenticationService()); // for now, would be OIDC in reality
  }

  @create()
  createDeploymentLoader(portalService: PortalService) : DeploymentLoader {
    return new EmptyDeploymentLoader() // only local, so far 
  }

  @create()
  createDeploymentManager(loader: DeploymentLoader, featureRegistry: FeatureRegistry) : DeploymentManager {
      return new DeploymentManager(
        featureRegistry,
        loader,
        manifest as Manifest // that's the local genaretd manifest.json
      );
  }

  // lifecycle

  @onRunning()
  async onRunning(featureRegistry: FeatureRegistry, deploymentManager: DeploymentManager, sessionManager: SessionManager<any,any>, routerManager: RouterManager) {
      // load deployment

      await deploymentManager.loadDeployment({
          application: "portal",
          client: deploymentManager.clientInfo(),
      });

      // the root if the router will be a feature with tag "portal" and the correct visibility

      routerManager.setRoot(featureRegistry.finder()
        .withTag('portal')
        .withVisibility(sessionManager.hasSession())
        .findOne());
        
      await sessionManager.init();
    }
}

// create environment

export const createEnvironment = async () : Promise<Environment> => {
    const environment = new Environment({module: ApplicationModule})

    await environment.start()

    return environment
}

The crucial parts are

DeploymentManager

The DeploymentManager which is responsible to compute a merged manifest.json. Since we are still completely local, it will only return the local manifest.json

FeatureRegistry

A FeatureRegistry collects all features and will be filled with the gathered information of the deployment manager. Since it knows about all registered components - local or remote - a <FeatureOutlet> component is now available that renders any registered feature by name, which is the basis for a number of mechanisms.

RoutingManager

The RoutingManager will compute dynamic routes based on the provided features and a handpicked "root" feature The routing logic will simply pick all features that have a defined "path" and add them as children - inserting a feature outlet - to the desired root feature, which in this case has a defined tag "portal" and has a visibility property that matches the current session state.

Launching the application is now just a couple lines of code.

Main File

const environment = await createEnvironment();

const root = createRoot(document.getElementById('root')!);
root.render(
    <EnvironmentContext.Provider value={environment}>
      <App />
    </EnvironmentContext.Provider>
    );

Application

while the application

export class App extends React.Component {
    routerManager!: RouterManager;

    static contextType = EnvironmentContext

    declare context: Environment

    async componentDidMount() { 
        this.routerManager = this.context.get(RouterManager);
    }

    override render() {
        return (
            this.routerManager.renderRouter()
        );
    }
}

Navigation

simply delegates to rendering to the defined routes. So what is the root? Well, a specific feature which acts as the main page typically offering navigation possibilities ( as a side bar ).

The interesting part, is that since we already have the complete meta-data available, we dont need to hardcode the navigation entries anymore, but can rely on a couple of conventions to list the available routes.

Example:

const features = featureRegistry
    .finder()
    .withPath()
    .withoutParent()
    .withVisibility(sessionManager.hasSession())
    .withTag('menu')
    .find();

In this case all features, that have the corresponding visibility status matching the session state and have a tag "menu", will be listed as corresponding <Link>s.

Isn't that awsome?

Ok, but we promised microfrontends as well, were are they?

Federated modules will take a similar approach, by defining a root module, exposing a manifest.json, etc. The main application will only need to change the corresponding deployment loader to integrate it.

  return new RemoteDeploymentLoader([
        { name: 'microfrontend', url: 'http://localhost:3001' },
      ]);

In this case, the manifest is fetched dynamically from the known url and merged with the local manifest.

This is good enough for a local environment used for development purposes, in production the logic would be handed over to a server component that is aware of different microfrontends and configurations also including more sophisticated logic to filter features according to feature flags, etc.

Showcase

A showcase app shows a shell and a microfreontend.

image

API Docs

API docs are available here

Clone this wiki locally