Extensible Web UI Plugin Architecture
Purpose
This document defines the high-level architecture and development guidelines for making the application's web UI extensible through plugins.
The main application consists of:
- a Rust backend;
- a Carbon-based web UI;
- a plugin host/runtime in the web UI;
- an optional backend proxy/service-resolution layer in the Rust application.
Plugins may:
- contribute pages, navigation entries, actions, panels, or other UI elements to the main application;
- render their own UI inside explicit extension points exposed by the host;
- call one or more REST APIs;
- use host-provided capabilities such as navigation, notifications, authentication-aware HTTP, and configuration.
The plugin UI and the plugin backend are independent deployables. A plugin backend may use any implementation technology, including Quarkus, Rust, Spring Boot, Go, or another platform.
1. Core Architectural Principles
1.1 The plugin contract is a browser API, not a framework API
The host must expose a stable plugin API that is independent of the host UI implementation details.
Plugins must not depend directly on internal Carbon component state, internal routing implementation, private stores, or undocumented DOM structure.
Prefer:
host.navigation.add(...)
host.pages.register(...)
host.http.get(...)
host.notifications.show(...)Avoid:
document.querySelector("#internal-menu").append(...)The host's implementation may use Carbon internally, but the public plugin contract must remain stable even if the host UI is later refactored.
1.2 Use ES modules as the primary plugin format
The default UI plugin format should be a JavaScript/TypeScript ES module.
A typical plugin package contains:
plugin/
├── plugin.json
├── plugin.js
├── plugin.css
└── assets/WebAssembly may be used internally by a plugin, but it should be treated as an implementation detail rather than the platform contract.
For example:
plugin.js
└── loads plugin.wasmThe host should only care that the plugin implements the supported ES module contract.
1.3 Plugins must use explicit extension points
Plugins should not receive unrestricted access to the host UI.
The host should define supported extension points such as:
- navigation entries;
- pages/routes;
- toolbar actions;
- dashboard widgets;
- detail-page tabs;
- context-menu actions;
- settings pages;
- notifications;
- command palette entries.
Each extension point should have a stable API and lifecycle.
1.4 Plugin UI and backend are separate deployables
A plugin UI must not assume that its backend is colocated with the UI artifact.
The plugin backend may be deployed:
- in the same Kubernetes cluster;
- in another cluster;
- behind a gateway;
- on another domain;
- as an external SaaS API.
The UI plugin should therefore refer to logical services rather than physical hostnames whenever possible.
1.5 Prefer host-mediated HTTP access
The preferred model is:
Plugin UI
|
| host.http.get("customer-api", "/customers")
v
Host Plugin API
|
| authentication
| authorization
| routing
| tracing
| tenant/context propagation
v
Rust backend / gateway
|
| reverse proxy / service resolution
v
Plugin backendThis avoids hard-coded backend hostnames and centralizes cross-cutting concerns.
2. High-Level Architecture
┌──────────────────────────────────────────────────────────────┐
│ Main Application │
│ │
│ Carbon Web UI │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Plugin Runtime │ │
│ │ │ │
│ │ - plugin discovery │ │
│ │ - manifest validation │ │
│ │ - lifecycle management │ │
│ │ - extension-point registration │ │
│ │ - capability enforcement │ │
│ │ - compatibility checks │ │
│ └──────────────────────────┬─────────────────────────────┘ │
│ │ │
│ v │
│ Plugin ES Modules │
│ │ │
│ │ host.http │
└─────────────────────────────┼────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────┐
│ Rust Backend │
│ │
│ - authentication │
│ - authorization │
│ - plugin configuration │
│ - service resolution │
│ - HTTP proxying │
│ - tracing / telemetry │
│ │
└─────────────────────────────┬────────────────────────────────┘
│
v
┌─────────────────────────┐
│ Plugin / External APIs │
│ │
│ Quarkus │
│ Rust │
│ Spring Boot │
│ SaaS APIs │
│ etc. │
└─────────────────────────┘3. Plugin Manifest
Every plugin should provide a machine-readable manifest.
Recommended name:
plugin.jsonExample:
{
"id": "customer-management",
"name": "Customer Management",
"version": "1.4.0",
"entrypoint": "./plugin.js",
"styles": ["./plugin.css"],
"requires": {
"hostApi": ">=1.0 <2.0",
"services": [
{
"id": "customer-api",
"version": "v1"
}
]
},
"permissions": [
"navigation",
"pages",
"notifications",
"service:customer-api:read",
"service:customer-api:write"
]
}The manifest should contain logical requirements only.
Do not hard-code environment-specific backend hostnames into the plugin manifest unless the API is intentionally external and public.
4. Plugin Lifecycle
Each plugin should implement a small lifecycle contract.
Example:
export interface Plugin {
activate(host: PluginHost): void | Promise<void>;
deactivate?(): void | Promise<void>;
}A plugin entry point may look like:
export async function activate(host) {
host.navigation.add({
id: "customers",
label: "Customers",
route: "/customers"
});
host.pages.register({
route: "/customers",
mount(container) {
return mountCustomerPage(container, host);
}
});
}
export async function deactivate() {
// release listeners, subscriptions, timers, and other resources
}The host should own plugin loading and unloading.
A plugin must clean up all resources it creates during deactivation.
5. Plugin Host API
The host API should be small, capability-oriented, versioned, and framework-independent.
A conceptual API may look like:
interface PluginHost {
version: string;
navigation: NavigationAPI;
pages: PageAPI;
toolbar: ToolbarAPI;
notifications: NotificationAPI;
http: HttpAPI;
config: ConfigurationAPI;
}Navigation
interface NavigationAPI {
add(entry: {
id: string;
label: string;
route: string;
icon?: string;
}): Disposable;
}Pages
interface PageAPI {
register(page: {
route: string;
mount(container: HTMLElement): void | Disposable;
}): Disposable;
}Notifications
interface NotificationAPI {
show(message: {
title?: string;
text: string;
kind?: "info" | "success" | "warning" | "error";
}): void;
}HTTP
interface HttpAPI {
get<T>(service: string, path: string): Promise<T>;
post<T>(service: string, path: string, body?: unknown): Promise<T>;
put<T>(service: string, path: string, body?: unknown): Promise<T>;
delete<T>(service: string, path: string): Promise<T>;
}The exact implementation is application-specific, but the public API should remain deliberately small.
6. UI Rendering Guidelines
6.1 The host owns the shell
The main application owns:
- global navigation;
- top-level routing;
- global theme;
- authentication UX;
- global notifications;
- application chrome;
- top-level error handling.
Plugins should contribute content inside host-controlled extension points.
6.2 Plugins own their mounted subtree
When a plugin page is mounted, the host provides a container:
mount(container: HTMLElement)The plugin may render its UI inside that container.
The plugin must not assume that it owns the entire document.
6.3 Carbon consistency
Because the host application uses Carbon, plugin authors should preferably use the same Carbon design system when building first-party plugins.
However, the plugin API should not require access to host-internal Carbon component instances.
Where practical, publish a supported UI package for plugin authors, for example:
@application/plugin-uiThis package may expose:
- supported Carbon component wrappers;
- theme utilities;
- spacing and typography tokens;
- common loading and error states;
- shared form patterns.
6.4 CSS isolation
Plugins should avoid global CSS.
Prefer one or more of:
- CSS modules;
- strict plugin-specific class prefixes;
- Shadow DOM where appropriate;
- host-provided style primitives.
A plugin must not redefine global selectors such as:
body { ... }
button { ... }
.bx--some-global-class { ... }unless explicitly supported by the plugin platform.
7. Backend Service Resolution
Plugins should use logical service identifiers.
Example:
await host.http.get("customer-api", "/customers");The UI plugin does not know whether the backend is physically located at:
http://customer-service.namespace.svc:8080or:
https://customer-api.prod.example.comThe platform resolves the logical name.
Recommended request path
Browser
|
| /api/plugins/customer-management/customers
v
Rust backend
|
| resolves logical service
v
customer-api
|
v
Plugin backendThe Rust backend may act as a reverse proxy for plugin services.
This provides a single browser origin and reduces CORS complexity.
8. Authentication and Authorization
Plugins should not directly manage primary application credentials.
The host HTTP capability should inject authentication information when appropriate.
For example:
Plugin
|
| host.http.get(...)
v
Host
|
| adds access token
| adds tenant context
| adds correlation ID
v
Plugin backendPermissions declared in plugin.json should be checked before activation.
Example:
{
"permissions": [
"service:customer-api:read",
"service:customer-api:write"
]
}The host should not expose a capability to a plugin unless the plugin has been granted the corresponding permission.
Frontend permission checks are not a substitute for backend authorization. Plugin backends must independently validate authorization for every protected operation.
9. Plugin Backend Guidelines
Plugin backends are independent services.
They should:
- expose stable REST APIs;
- publish an OpenAPI specification when possible;
- implement their own authorization checks;
- support distributed tracing/correlation IDs;
- avoid relying on the physical location of the UI plugin;
- maintain backward compatibility for supported API versions.
The plugin UI should consume the backend through an API contract rather than shared implementation details.
Recommended:
OpenAPI
|
+-- backend validation/tests
|
+-- generated TypeScript client or typed API model
|
v
Plugin UI10. Testing Strategy
Testing should be possible without running the full application.
10.1 Plugin unit tests
Provide a mockable test host.
Example:
const host = createTestHost();
await activate(host);
expect(host.navigation.entries()).toContainEqual({
id: "customers",
label: "Customers",
route: "/customers"
});10.2 Plugin test kit
Publish a reusable test package, for example:
@application/plugin-testkitIt should verify:
- manifest validity;
- successful activation;
- successful deactivation;
- duplicate extension IDs;
- route validity;
- permission declarations;
- resource cleanup;
- supported host API usage;
- compatibility with supported API versions.
10.3 Fake development host
Provide a small development application that can load a plugin independently from the real product.
It should allow plugin developers to test:
- navigation contribution;
- pages;
- light/dark themes;
- permissions;
- mocked backend responses;
- backend failures;
- latency;
- authentication states;
- multiple host API versions.
10.4 Browser integration tests
Use a browser test framework to run:
Fake Host + Real Plugin + Mock Backendand:
Fake Host + Real Plugin + Real Plugin Backend10.5 Full-system tests
Keep a smaller set of tests using:
Real Host + Real Plugin + Real Plugin BackendThese should validate critical end-to-end workflows rather than every UI detail.
11. Compatibility and Versioning
The plugin host API must be explicitly versioned.
Example:
{
"requires": {
"hostApi": ">=1.1 <2.0"
}
}The host must validate compatibility before loading the plugin.
Avoid silently changing existing behavior within a host API version.
Prefer additive evolution:
1.0
+ navigation
+ pages
1.1
+ notifications
1.2
+ toolbarBreaking changes should require a new major version.
The test kit should allow plugins to run compatibility tests against every supported host API version.
12. Plugin Discovery and Installation
The application may support one or more discovery mechanisms:
- static configuration;
- server-side plugin registry;
- filesystem or deployment configuration;
- remote plugin catalog.
A plugin installation record should associate:
plugin identity
|
+-- UI artifact location
|
+-- logical services
|
+-- granted permissions
|
+-- configurationExample server-side configuration:
plugins:
- id: customer-management
manifest: https://plugins.example.com/customer/1.4.0/plugin.json
services:
customer-api:
target: http://customer-service:8080
permissions:
- navigation
- pages
- service:customer-api:readThis environment-specific configuration belongs to the platform, not the plugin bundle.
13. Security Guidelines
Treat plugins as separate trust boundaries.
The host should:
- validate plugin manifests;
- enforce supported host API versions;
- grant only declared and approved capabilities;
- restrict backend access to approved logical services;
- validate service paths where appropriate;
- apply authorization server-side;
- apply CSP and integrity controls where appropriate;
- log plugin activation and relevant API operations;
- avoid exposing internal application state directly.
For trusted first-party plugins, an ES module loaded into the main page may be sufficient.
For untrusted third-party plugins, stronger isolation may be required, such as:
sandboxed iframe
+
message-based host APIDo not assume that WebAssembly alone provides DOM or browser security isolation.
14. Error Handling
A plugin failure should not make the entire host UI unusable.
The plugin runtime should isolate plugin lifecycle failures and present a controlled error state.
Example:
Plugin failed to load
Customer Management could not be started.
[Retry]The host should catch:
- manifest failures;
- module loading failures;
- activation failures;
- route rendering failures;
- backend request failures;
- compatibility failures.
Plugins should use host-provided error and notification primitives where available.
15. Observability
Plugin-originated operations should be observable across frontend and backend boundaries.
Prefer propagating:
- correlation IDs;
- trace IDs;
- plugin ID;
- plugin version;
- user/session context where appropriate.
Conceptually:
Plugin UI
|
| plugin=customer-management
| trace-id=...
v
Rust Backend
|
v
Plugin BackendThis makes debugging plugin/backend interactions significantly easier.
16. Recommended Repository Responsibilities
Main application repository
The main application should own:
plugin-api/
plugin-runtime/
plugin-testkit/
plugin-development-host/
plugin-manifest-schema/
service-resolution/Responsibilities include:
- defining the plugin contract;
- maintaining compatibility;
- loading plugins;
- providing host capabilities;
- proxying/resolving backend services;
- enforcing permissions;
- providing the plugin development environment.
Plugin repository
A plugin repository should typically contain:
ui/
plugin.json
src/
tests/
backend/ # optional and independent deployable
...The UI should depend only on public plugin SDK packages and documented REST API contracts.
The backend should not depend on internal host UI implementation details.
17. Recommended Initial Scope
For the first version, keep the platform deliberately small.
Recommended initial capabilities:
Plugin lifecycle
activate
deactivate
UI
navigation.add
pages.register
Host services
http.get/post/put/delete
notifications.show
Platform
manifest
permissions
host API versioning
development host
test kitAvoid adding highly generic extension mechanisms until concrete use cases require them.
For example, prefer:
host.navigation.add(...)over a generic API such as:
host.extend("anything", {...})Explicit contracts are easier to version, test, document, and secure.
18. Decision Summary
The recommended architecture is:
Main Application
Rust Backend + Carbon Web UI
|
v
Stable Plugin API
|
v
ES Module Plugin
|
+---------+----------+
| |
v v
UI extensions host.http(...)
|
v
Rust backend proxy
|
v
Plugin / External REST APIKey decisions:
- Use ES modules as the default plugin packaging model.
- Keep WebAssembly optional and internal to individual plugins.
- Expose explicit extension points rather than arbitrary DOM manipulation.
- Keep the plugin UI independent from its backend deployable.
- Use logical service identifiers instead of backend hostnames.
- Route REST access through a host-provided HTTP capability when possible.
- Centralize authentication, authorization context, routing, and observability in the host/platform.
- Publish a plugin SDK, test kit, and fake development host.
- Version the host plugin API and test compatibility explicitly.
- Keep the first plugin API small and evolve it through concrete extension requirements.
The most important boundary is:
A plugin should depend on the public plugin API and its backend API contracts, never on the private implementation details of the main application.