Experience Plugins — Storage & Deployment Guide
Purpose
Document how plugins are built, stored, configured, served, and operated in the Experience platform using file‑backed, same‑origin ESM bundles served by the BFF.
Scope
- Experience SPA host and plugin system
- BFF plugin storage, serving, and enforcement
- Dev workflow and DevOps operations for deploying, updating, and rolling back plugins
1) High‑level model
- Plugins are authored outside the host app and built as same‑origin ESM bundles.
- The BFF serves:
- Manifests from
ServiceConfigs/BFF/config/plugins.yaml - Bundles from file paths mounted read‑only under
/app/plugins
- Manifests from
- The SPA:
- Discovers manifests → batches PDP pre‑gating → dynamically imports file‑backed bundles → mounts contributions
- Runtime calls from plugins are enforced by the BFF via method+path allow‑lists and
X-Plugin-Id(see Plugins reference).
2) Storage layout (source of truth)
- Host filesystem (checked into your configuration repo):
ServiceConfigs/BFF/plugins/<pluginId>/<version>/index.esm.js
- Container mount (BFF):
/app/plugins(read‑only)
- Configuration (BFF):
ServiceConfigs/BFF/config/plugins.yaml→ declares each plugin plus its bundle file path
Compose mounts
- docker‑compose‑authzen4.yml (BFF):
../ServiceConfigs/BFF/plugins:/app/plugins:ro../ServiceConfigs/BFF/config:/app/config:ro
- docker‑compose‑nowconnect.yml (BFF):
../ServiceConfigs/BFF/plugins:/app/plugins:ro../ServiceConfigs/BFF/config:/app/config:ro
3) Plugin config: ServiceConfigs/BFF/config/plugins.yaml
Example (hello, page, and widget plugins):
tenants:
experience.ocg.labs.empowernow.ai:
- id: hello
version: "1.0.0"
engine:
experience: ">=1.0.0"
bundle:
file: "/app/plugins/hello/1.0.0/index.esm.js"
# integrity: "sha256:<optional_hex_hash>"
permissions:
api:
- method: GET
path: /api/plugins/secure-echo
- method: POST
path: /api/plugins/telemetry
sse: []
contributions:
routes:
- path: /hello
component: Hello
resource: plugin.route
action: view
widgets:
- slot: dashboard.main
component: HelloWidget
resource: plugin.widget
action: view
- id: hello-page
version: "1.0.0"
engine:
experience: ">=1.0.0"
bundle:
file: "/app/plugins/hello-page/1.0.0/index.esm.js"
permissions:
api:
- method: GET
path: /api/plugins/secure-echo
sse: []
contributions:
routes:
- path: /hello-page
component: HelloPage
resource: plugin.route
action: view
- id: hello-widget
version: "1.0.0"
engine:
experience: ">=1.0.0"
bundle:
file: "/app/plugins/hello-widget/1.0.0/index.esm.js"
permissions:
api:
- method: POST
path: /api/plugins/telemetry
sse: []
contributions:
widgets:
- slot: dashboard.main
component: HelloWidget
resource: plugin.widget
action: view
Key fields
id/version/engine.experience: identity and compatibility rangebundle.file: absolute file path inside the container under/app/pluginsbundle.integrity(optional): content hash enforced at serve timepermissions: allow‑list of method+path templates and SSE prefixescontributions: routes and/or widgets with PDP hints
4) Bundle contract (file‑backed ESM)
Minimal examples (stored under ServiceConfigs/BFF/plugins/...):
Hello Page bundle (hello-page/1.0.0/index.esm.js)
import * as React from 'react';
export const routes = {
HelloPage: () => React.createElement(
'div',
{ className: 'glass-card p-4' },
'Hello World Page'
)
};
export default { routes };
Hello Widget bundle (hello-widget/1.0.0/index.esm.js)
import * as React from 'react';
export const widgets = {
HelloWidget: () => React.createElement(
'div',
{ className: 'glass-card p-2' },
'Hello World Widget'
)
};
export default { widgets };
Authoring rules
- Export
routesand/orwidgetsas plain objects of React components. - Treat
react,react-dom, and@empowernow/uias externals; they are provided by the host. - No global CSS or external scripts; CSP remains strict (
script-src 'self').
5) SPA integration
- Manifests:
GET /api/plugins/manifests(cookie‑auth, same‑origin) - Import: SPA uses dynamic import on a URL pointing to the plugin bundle.
Current SPA loader URL pattern
- The loader requests:
/api/plugins/bundle?entry=<pluginId>&id=<pluginId>
- Examples:
/api/plugins/bundle?entry=hello-page&id=hello-page/api/plugins/bundle?entry=hello-widget&id=hello-widget
Note: The BFF maps id to the configured bundle.file and streams the file back as text/javascript.
6) BFF serving logic (what changed)
- Manifests
- Loaded from
plugins.yamlinto memory per tenant host. - Allow‑lists compiled for runtime request enforcement.
- Loaded from
- Bundles
- For each request to
/api/plugins/bundle?...:- Check quarantine → 403 with
X-Plugin-Quarantined: 1if blocked. - Resolve
idto its configuredbundle.file(fromplugins.yaml). - Root‑jail: Only serve files under
/app/plugins. - Integrity (optional): if
integrityis set, verify the sha256 of the file content; on mismatch return 409 withX-Integrity-Error: 1. - Serve via
FileResponsewith:Content-Type: text/javascript; charset=utf-8ETag: sha256-<hex>Cache-Control: public, max-age=31536000, immutableX-Content-Type-Options: nosniffCross-Origin-Resource-Policy: same-origin
- Check quarantine → 403 with
- For each request to
- Authentication
- Route is
auth: sessioninroutes.yaml, so an authenticated session is required to fetch bundles.
- Route is
- Security
- Same‑origin ESM: no CSP relaxation needed.
- Root‑jail ensures no path traversal outside
/app/plugins.
7) Runtime enforcement (recap)
- BFF Middleware enforces allow‑lists per plugin:
- HTTP calls must include header
X-Plugin-Id: <id> - Method+path validated against templates compiled from
permissions.api - SSE prefixes validated against
permissions.sse - Violations return 403 with
X-Allowlist-Violation: 1.
- HTTP calls must include header
- SPA pre‑gates at render time using AuthZEN batch calls, so contributions are mounted only when permitted.
8) Developer workflow
Build
- Author routes and/or widgets and export as ESM.
- Externalize
react,react-dom,@empowernow/ui. - Produce a single file bundle (e.g.,
index.esm.js).
Place
- Copy the built bundle to:
ServiceConfigs/BFF/plugins/<pluginId>/<version>/index.esm.js
Configure
- Add/Update an entry under
ServiceConfigs/BFF/config/plugins.yaml:- Set
id,version,engine.experience. - Set
bundle.fileto/app/plugins/<pluginId>/<version>/index.esm.js. - Add
contributionsandpermissions.
- Set
Deploy
- The BFF containers mount:
../ServiceConfigs/BFF/plugins:/app/plugins:ro../ServiceConfigs/BFF/config:/app/config:ro
- Restart/reload the BFF to pick up config changes (and bundles).
Verify
- Hit
GET /api/plugins/manifests(authenticated session) and confirm your plugin appears. - Navigate to the route or widget location in the Experience SPA.
- Inspect the Network tab for
GET /api/plugins/bundle?...with 200 and strong caching headers.
9) Operations & DevOps
Atomic rollout (recommended)
- Stage new bundle files under
ServiceConfigs/BFF/plugins/.staged/... - After pushing the config entry, flip the directory or symlink in one step to the final
/app/plugins/<id>/<version>path to avoid broken references.
Integrity management (optional but recommended)
- Compute a sha256 hash of the final ESM file and record it as
bundle.integrity: "sha256:<hex>" - On mismatch, the BFF fails closed with 409 and
X-Integrity-Error: 1.
Quarantine
- Immediate kill switch to block serving and use:
POST /api/plugins/quarantine/{plugin_id}POST /api/plugins/unquarantine/{plugin_id}
Hot reload of manifests
POST /api/plugins/refreshtriggers the BFF to reloadplugins.yaml.
Monitoring (minimum recommended)
- Track bundle serve outcomes and latencies by
{tenant, plugin_id, version}. - Track allow‑list denials and quarantine events.
Backups
- Treat
ServiceConfigsas the configuration SoT; ensure your normal config backup processes include the plugins folder andplugins.yaml.
10) Security posture
- Same‑origin ESM; strict CSP.
- No runtime tokens in browser; cookies + CSRF via BFF.
- PDP pre‑gating at render time; BFF allow‑list enforcement at request time.
- Root‑jail for bundles (
/app/plugins). - Optional integrity checks on bundles.
11) Example end‑to‑end (Hello Page + Hello Widget)
Files
ServiceConfigs/BFF/plugins/hello-page/1.0.0/index.esm.js(exportsroutes.HelloPage)ServiceConfigs/BFF/plugins/hello-widget/1.0.0/index.esm.js(exportswidgets.HelloWidget)
Configuration
ServiceConfigs/BFF/config/plugins.yamlentries forhello-pageandhello-widgetpointing to/app/plugins/.../index.esm.js, with contributions and permissions.
Runtime
- SPA discovers manifests, then imports:
/api/plugins/bundle?entry=hello-page&id=hello-page/api/plugins/bundle?entry=hello-widget&id=hello-widget
- BFF serves file‑backed ESM with long‑lived immutable caching and ETags.
- Contributions mount only when permitted by PDP decisions.
12) Developer checklist
- Build as ESM; exports
routesand/orwidgets. - No external UI kits; use
@empowernow/ui. - Externals:
react,react-dom,@empowernow/ui. - Place under
ServiceConfigs/BFF/plugins/<id>/<version>/index.esm.js. - Configure
plugins.yamlwithbundle.file, contributions, and permissions. - Restart/reload BFF; verify
manifestsand network import URL. - Ensure PDP pre‑gating passes for your test user; mount points render.
- Calls go through the SDK and carry
X-Plugin-Id; 403s indicate allow‑list or authz issues.
13) Appendix – API reference (BFF‑facing)
GET /api/plugins/manifests- Returns
PluginManifest[]scoped by tenant host - Headers:
Vary: Cookie, X-Plugin-Id(for manifests, not bundles)
- Returns
GET /api/plugins/bundle?entry=<id>&id=<id>- Returns ESM bundle file
- Headers:
Content-Type: text/javascript; charset=utf-8ETag: sha256-<hex>Cache-Control: public, max-age=31536000, immutableX-Content-Type-Options: nosniffCross-Origin-Resource-Policy: same-origin
- Errors:
- 403 with
X-Plugin-Quarantined: 1if quarantined - 404 unknown plugin
- 409 with
X-Integrity-Error: 1on integrity mismatch
- 403 with
POST /api/plugins/refresh→ reload manifests from configPOST /api/plugins/quarantine/{id}POST /api/plugins/unquarantine/{id}
14) Notes on compatibility
- The SPA loader continues to use query params; no host code changes required.
- Future‑proofing: if you later switch to path params (e.g.,
/api/plugins/bundle/{pluginId}/{version}) you can run both handlers in parallel and migrate the SPA loader when convenient.
See also:
- Experience → Plugins:
./plugins - Experience → Plugin Development Guide:
./plugin_guide - BFF → Experience Routing & Config:
../bff/devops/experience_routing