Frontend UI Mount Integration Guide
This guide explains how a frontend shell application integrates with the MetaOne platform to dynamically load and render extension UIs.
Overview
Extensions declare their UI through a manifest at install time. The platform stores these declarations and exposes them via a UI Mount API. The frontend shell queries this API on login, then uses the response to build navigation, register routes, and render widgets — without any hard-coded knowledge of which extensions exist.
Extension UI assets (JavaScript bundles) are served by the platform itself and require authentication, so the browser never needs direct access to the extension's origin.
Platform API Contract
Extensions are pure UI shells. They do not hold authentication tokens, construct backend URLs, or make HTTP calls. Instead, the host shell creates a platform object from its own auth context and passes it as a prop when mounting each remote component.
The platform prop
Every remote module (route page, widget) receives a single platform prop with the following shape:
| Field | Type | Description |
|---|---|---|
workspaceId | string | Current workspace identifier |
invokeCapability(key, payload?) | (string, object?) → Promise<any> | Calls a registered extension capability |
publishEvent(eventKey, payload?) | (string, object?) → Promise<void> | Publishes a platform event |
The extension never stores tokens, constructs API URLs, or uses fetch directly. All backend communication flows through the platform prop.
How the host constructs platform
The host shell creates the platform object using its own session state (current user token, selected workspace). A minimal implementation:
invokeCapabilityposts toPOST /api/workspaces/{workspaceId}/capabilities/{key}with the JWT in the Authorization header.publishEventposts toPOST /api/workspaces/{workspaceId}/events/publishwith the event key and payload.
The extension never sees any of this. It simply calls platform.invokeCapability('crm.customer.search') and receives the resolved data.
When to use each method
- Use
invokeCapabilityfor queries and commands: load a list, create a record, search, fetch details. - Use
publishEventfor side-effect events: user sent a message, a deal changed stage, a notification should be dispatched. Events may trigger reactions in other extensions.
Step 1 — Fetch UI Mounts
After the user logs in and a workspace is selected, call the UI mounts bundle endpoint. This single call returns all navigation items, routes, and widgets registered by every enabled extension in that workspace.
Endpoint: GET /api/catalog/workspaces/{workspaceId}/ui-mounts
The response contains three lists:
nav— items to render in the sidebar or top navigation bar. Each item has a label, icon name, target route, and display order.routes— URL path patterns the shell must register. Each entry maps a path pattern to an extension key.widgets— injectable components identified by a named mount point. Each entry has an extension key, a mount point name, and a display order.
You can also fetch each list individually if needed:
GET .../ui-mounts/navGET .../ui-mounts/routesGET .../ui-mounts/widgets— accepts an optionalmountPointquery parameter to filter
Step 2 — Derive Asset URLs
The API response never includes asset URLs directly. The frontend constructs them from the extension key using a fixed pattern:
/api/catalog/workspaces/{workspaceId}/ui-mounts/{extensionKey}/assets/{filename}The entry point for Module Federation is always remoteEntry.js, so the URL to load an extension's remote container is:
/api/catalog/workspaces/{workspaceId}/ui-mounts/{extensionKey}/assets/remoteEntry.jsThe platform returns 403 if the extension is not installed and enabled in the requested workspace, so the URL itself is safe to use as a capability check.
Step 3 — Load Remote Modules
The platform uses Vite Module Federation (@originjs/vite-plugin-federation). Each extension publishes a remoteEntry.js that exposes named modules. The shell loads these at runtime via dynamic import.
Each extension exposes at least ./App — the main page component loaded when the user navigates to the extension's route. Extensions that register widgets expose additional named modules, one per widget.
The exposed module names for each currently registered extension are:
| Extension | Exposes | Used for |
|---|---|---|
crm-extension | ./App | Route /ext/crm-demo/* |
crm-extension | ./CrmDashboardWidget | Mount point crm.dashboard.widget |
chat-extension | ./App | Route /ext/chat/* |
When adding support for a new extension, check its manifest or ask the extension author which module names it exposes.
Step 4 — Register Routes Dynamically
For each entry in the routes list, register a route in the frontend router after fetching the mounts. The path patterns use /* wildcards; translate these to your router's syntax (Vue Router uses :pathMatch(.*)*).
Each route should lazily load the extension's ./App module using the asset URL derived in Step 2. This ensures the extension bundle is only fetched when the user navigates to that route.
When mounting the route component, pass the platform prop so the extension can interact with the backend.
Step 5 — Render Navigation
Render nav items from the nav list in display order (ascending by order). Each item provides:
label— display texticon— icon identifier (use your icon library's lookup)route— the path to navigate to when clickedorder— sort position among all extension nav items
Shell-defined nav items (home, settings, etc.) are static; extension nav items slot in alongside them.
Step 6 — Render Widget Mount Points
Place a widget slot component in any view where extensions can inject content. The slot is identified by a mount point name — a string like crm.dashboard.widget or customer.profile.tab.
At runtime, the slot queries the widgets list for entries matching its mount point name, sorts them by order, and renders each one by loading the corresponding named module from the extension's remote container.
Pass the platform prop to each widget component so it can load data and react to user interactions.
The shell does not need to know in advance which extensions will fill a slot. If no extension registers for a mount point, the slot renders nothing.
Refresh Behavior
UI mounts should be re-fetched whenever:
- The user switches to a different workspace
- The user installs or enables an extension (the admin flow should trigger a refresh)
- The session is restored after a page reload
Mounts do not change during a normal session unless an admin action occurs. Polling is not required.
Error Handling
| Scenario | Expected behavior |
|---|---|
| Extension asset returns 403 | Extension is not enabled in this workspace; skip loading |
| Extension asset returns 404 | Asset file missing in plugin directory; show error or skip |
| Remote module fails to load | Catch the dynamic import error; render a fallback or nothing |
| UI mounts endpoint returns empty lists | No extensions installed; render shell only |
Currently Registered Extensions
| Extension key | Nav label | Nav route | Widget mount point |
|---|---|---|---|
crm-extension | CRM | /ext/crm-demo | crm.dashboard.widget |
chat-extension | Chat | /ext/chat | (none) |