Experience Platform Architecture
Master the unified end-user portal: plugin architecture, excellence playbook, theming, SDK APIs, and creating plugins from scratch.
📋Learning Objectives
- Understand the Experience platform architecture and how it differs from traditional portals
- Learn how plugins are discovered, loaded, and secured via ARIA Shield
- Master the 8 Pillars of Plugin Excellence
- Understand the 9-theme system and CSS variable approach
- Use the Plugin SDK for API calls, authorization, and real-time updates
- Create a production-ready plugin from scratch in 5 minutes
🏗️Experience Platform Overview
What Makes Experience Different
- PDP-Aware UI: Every route, widget, and action is governed by OpenID AuthZEN decisions through ARIA Shield. Provable least-privilege UX.
- Runtime Module Activation: An ARIA Shield-served config enables/disables app modules on the fly—no rebuilds for tenants.
- Zero-Token SPA: Session in httpOnly cookies; no access tokens in browser. Eliminates token exfiltration.
- CSP-Safe Plugins: Plugins are ESM bundles loaded via ARIA Shield—no CSP relaxations or cross-origin scripts.
- Design System Consistency: Neon Flux tokens/components ensure premium, coherent UI across all plugins.
The Experience platform is a modular React application that hosts plugins:
(React Router, Auth)"] PluginHost["Plugin Host"] SharedUI["Shared UI Library"] end subgraph Plugins["🧩 Plugins"] P1["Plugin A"] P2["Plugin B"] P3["Plugin C"] end subgraph Backend["🔧 Backend Services"] BFF["BFF Gateway"] IdP["Identity Provider"] PDP["Policy Decision Point"] end Shell --> PluginHost PluginHost --> P1 & P2 & P3 SharedUI --> P1 & P2 & P3 P1 & P2 & P3 --> BFF BFF --> IdP & PDP style Experience fill:#7c3aed,color:#fff style Plugins fill:#0891b2,color:#fff style Backend fill:#059669,color:#fff
Key Components
- App Shell: Main application frame with React Router, authentication, and theme management
- ARIA Shield (BFF): Security gateway handling auth, PDP evaluation, and API proxying
- Plugin Host: Dynamically loads and mounts plugins with isolation
- Shared UI Library: Common components (buttons, forms, tables) with theming built-in
- Plugin SDK: Hooks and utilities for API calls, authorization, and telemetry
https://experience.self.empowernow.ai
All traffic routes through ARIA Shield—plugins never communicate directly with backend services.
📦Plugin Loading Mechanism
Plugins are loaded dynamically at runtime using module federation:
Loading Steps
- Discovery: Shell fetches available plugins from registry
- Manifest: Each plugin has a manifest.json with metadata
- Loading: Plugin bundle loaded via dynamic import
- Mounting: Plugin React component mounted in host container
🛣️Route Registration
Plugins register routes via their manifest:
{
"name": "role-viewer",
"routes": [
{
"path": "/roles",
"component": "RoleListPage",
"title": "Role List",
"icon": "users"
},
{
"path": "/roles/:id",
"component": "RoleDetailPage",
"title": "Role Details"
}
]
}
All plugin routes are automatically prefixed with /plugins/{plugin-name}. So /roles becomes /plugins/role-viewer/roles.
📄Manifest Format (Plugin Config)
ServiceConfigs/BFF/config/plugins/index.yaml — Tenant registry (which plugins are enabled per tenant)
ServiceConfigs/BFF/config/plugins/manifests/<plugin-id>.yaml — Per-plugin manifest (routes, permissions, security)
Example tenant registry in index.yaml:
tenants:
- tenant_id: "*"
plugins:
- id: my-plugin
title: "My Plugin"
version: "1.0.0"
enabled: true
# 🔐 Security configuration (REQUIRED)
authorization:
pdp_application: "my-plugin" # Unique scope for isolation
context:
service_tier: "standard" # free/standard/premium/enterprise
compliance_level: "medium" # low/medium/high/critical
data_classification: "general" # public/general/internal/pii/secret
# 🔌 Bundle configuration
bundle:
file: "/app/plugins/my-plugin/1.0.0/index.esm.js"
integrity: "sha256:abc123..." # Verified on load
# 🔒 API permissions (declare all endpoints)
permissions:
api:
- method: POST
path: /api/crud/execute
- method: POST
path: /access/v1/evaluation
# 🗺️ Routes
contributions:
routes:
- path: /my-plugin
component: Dashboard
title: "My Plugin"
Security Model
- pdp_application: Isolates authorization decisions—plugin A can't affect plugin B
- permissions.api: Allowlist of endpoints—requests outside this list get 403
- bundle.integrity: SHA256 hash verified before loading—prevents tampering
🏆The 8 Pillars of Plugin Excellence
Never show UI without checking authorization. Use Protected.Button and IfCan components—they handle loading, errors, and hide unauthorized elements automatically.
// ❌ BAD: 25 lines of manual auth checking
const [canDelete, setCanDelete] = useState(false);
useEffect(() => { /* complex auth logic */ }, []);
// ✅ GOOD: 1 line with SDK
<Protected.Button resource="user" action="delete" onClick={handleDelete}>
Delete User
</Protected.Button>
Use the SDK's crud.use() hook for data fetching. It handles caching, loading states, and error handling automatically.
const { data, isLoading, error } = sdk.crud.use(
'av_openldap', 'account', 'get_all_users',
{ page_size: 50 }
);
Every async operation needs a loading state. Users should never see blank screens or wonder if something is happening.
Fail gracefully with actionable error messages. Never show raw stack traces or "Something went wrong."
Hover effects, smooth transitions, and subtle animations make the difference between "functional" and "premium."
Power users expect shortcuts. Ctrl+N for create, Esc for close, Enter to submit.
Never hardcode colors. Always use CSS variables. Your plugin must work across all 9 themes.
ARIA labels, keyboard navigation, focus indicators. The SDK components have this built-in.
🎨The 9-Theme System
Available Themes
CSS Variables You MUST Use
// Page & Surfaces
background: 'var(--main-bg)' // Page background
background: 'var(--card-bg)' // Card backgrounds
color: 'var(--main-text)' // Primary text
color: 'var(--main-text-secondary)' // Muted text
// Interactive Elements
background: 'var(--button-bg)' // Can be gradient!
border: '1px solid var(--border-color)'
boxShadow: 'var(--shadow-md)'
// State Colors
color: 'var(--error-color)' // Errors
color: 'var(--success-color)' // Success
color: 'var(--warning-color)' // Warnings
Never hardcode colors! background: '#1a1a1a' is invisible on dark themes. color: '#ffffff' is invisible on light themes. Always use CSS variables.
🔌Plugin SDK Reference
SDK Namespaces
| Namespace | Purpose | Key Methods |
|---|---|---|
sdk.api | HTTP requests | fetch(), useQuery() |
sdk.authz | Authorization | evaluate(), batchEvaluate() |
sdk.crud | Data operations | use(), execute(), run() |
sdk.sse | Real-time updates | subscribe(), unsubscribeAll() |
sdk.telemetry | Analytics | track() |
API Fetch (with automatic X-Plugin-Id header)
// GET request
const response = await sdk.api.fetch('/crud/tasks');
const tasks = await response.json();
// POST with body
const response = await sdk.api.fetch('/crud/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'New task' })
});
Authorization Checks
// Single evaluation
const result = await sdk.authz.evaluate({
resource: { type: 'workflow', id: 'wf-123' },
action: { name: 'start' }
});
const allowed = result.decision === true;
// Batch evaluation (3x faster!)
const results = await sdk.authz.batchEvaluate([
{ resource: { type: 'user', id: userId }, action: { name: 'edit' } },
{ resource: { type: 'user', id: userId }, action: { name: 'delete' } }
]);
CRUD Operations
// Data fetching hook
const { data, isLoading, error } = sdk.crud.use(
'av_openldap', 'account', 'get_all_users',
{ page_size: 50 }
);
// Execute command
const result = await sdk.crud.execute(
'av_openldap', 'account', 'get_user_by_entryUUID',
{ SystemIdentifier: userId }
);
🔬Hands-on Labs
Let's examine how existing plugins are structured.
Navigate to the plugins configuration (replace <YourRepoRoot> with your project root):
# Windows — tenant registry
type <YourRepoRoot>\ServiceConfigs\BFF\config\plugins\index.yaml
# macOS/Linux — tenant registry
cat <YourRepoRoot>/ServiceConfigs/BFF/config/plugins/index.yaml
# Browse per-plugin manifests
ls <YourRepoRoot>/ServiceConfigs/BFF/config/plugins/manifests/
List enabled plugins:
Look for the tenants section and identify which plugins are enabled: true.
Examine a plugin's bundle directory:
# Windows
dir <YourRepoRoot>\ServiceConfigs\BFF\plugins
# macOS/Linux
ls <YourRepoRoot>/ServiceConfigs/BFF/plugins
- What
pdp_applicationdoes each plugin use? - What API permissions does each plugin declare?
- What routes does each plugin contribute?
Build a production-ready plugin using the Excellence Playbook patterns.
Create the plugin directory (replace <YourRepoRoot> with your project root):
mkdir -p <YourRepoRoot>/plugins/training-plugin/1.0.0/src
cd <YourRepoRoot>/plugins/training-plugin/1.0.0
Windows: use backslashes and mkdir if needed; or use the same mkdir -p in Git Bash/WSL.
Initialize package.json:
{
"name": "training-plugin",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "esbuild src/index.tsx --bundle --format=esm --platform=browser --target=es2020 --outfile=dist/index.esm.js --external:react --external:react-dom --minify"
},
"devDependencies": {
"esbuild": "^0.21.0"
}
}
Create src/index.tsx with theme-aware styling:
const React = (globalThis as any).React;
export function Dashboard() {
return React.createElement('div', {
style: {
padding: '24px',
background: 'var(--main-bg)',
color: 'var(--main-text)',
minHeight: '100vh'
}
},
React.createElement('h1', {
style: {
fontSize: '28px',
marginBottom: '16px',
color: 'var(--main-text)'
}
}, '🎓 Training Plugin'),
React.createElement('div', {
style: {
background: 'var(--card-bg)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
padding: '20px'
}
}, 'Welcome to your first plugin!')
);
}
export const routes = { Dashboard };
Build and deploy (replace <YourRepoRoot> with your project root):
npm install
npm run build
# Generate integrity hash (PowerShell)
(Get-FileHash dist/index.esm.js -Algorithm SHA256).Hash.ToLower()
# Copy to ServiceConfigs — Windows
copy dist\index.esm.js <YourRepoRoot>\ServiceConfigs\BFF\plugins\training-plugin\1.0.0\
# macOS/Linux
cp dist/index.esm.js <YourRepoRoot>/ServiceConfigs/BFF/plugins/training-plugin/1.0.0/
Add to plugins.yaml and restart BFF
Navigate to https://experience.self.empowernow.ai/training-plugin and see your plugin running with proper theming!
🔧Troubleshooting Plugin Issues
Plugin loads, but API calls return 403 Forbidden. Response has header X-Allowlist-Violation: 1.
- Check the
permissions.apisection in plugins.yaml - Verify the path matches exactly (including leading /api/)
- Check the HTTP method (GET vs POST)
permissions:
api:
- method: POST
path: /api/crud/execute # Add missing endpoint
Plugin fails to load. Console shows "Bundle integrity check failed."
The SHA256 hash in plugins.yaml doesn't match the actual bundle file.
# Regenerate hash (PowerShell)
$hash = (Get-FileHash dist/index.esm.js -Algorithm SHA256).Hash.ToLower()
Write-Host "sha256:$hash"
# Update plugins.yaml with new hash
Plugin looks fine on dark theme, but text is invisible on light theme (white-on-white).
Hardcoded colors in the plugin code.
// ❌ BAD
color: '#ffffff'
// ✅ GOOD
color: 'var(--main-text)'
❓Knowledge Check
Experience is PDP-aware at the widget/action level, not just route-level. Every button, every menu item can be governed by authorization policies. It also supports workflow/task execution and real-time updates—not just SSO tiles.
ARIA Shield is the Backend-for-Frontend (BFF) that handles authentication, authorization, and API proxying. Plugins communicate through it because:
- Session cookies are httpOnly—no tokens in browser JavaScript
- API requests are validated against the plugin's permission allowlist
- All requests get the X-Plugin-Id header for audit trails
It defines the policy namespace for the plugin. Authorization decisions are scoped to this application, so Plugin A's policies don't affect Plugin B. It's how multi-tenant plugins achieve isolation.
The platform supports 9 different themes (5 dark, 4 light). Hardcoded colors break on theme switches—white text on white background, or dark text on dark background. CSS variables automatically adapt to the current theme.
It's a SHA256 hash of the plugin bundle. ARIA Shield verifies this hash before loading the plugin, preventing:
- Tampering with plugin bundles
- Man-in-the-middle attacks
- Accidental deployment of wrong version
Protected.Button is 1 line vs ~25 lines for manual auth. It handles:
- Authorization check (auto-hides if denied)
- Loading state (spinner while checking)
- Error handling (toast on failure)
- Accessibility (ARIA labels)
- Hover animations
- Business logging
Use the SDK's sse.subscribe() method for Server-Sent Events:
useEffect(() => {
const unsubscribe = sdk.sse.subscribe('/events/tasks', data => {
setTasks(data);
});
return unsubscribe; // Cleanup on unmount
}, []);
Remember to declare the SSE endpoint in permissions.sse in plugins.yaml.
📝Day 11 Checkpoint
- Understand what makes Experience different from typical portals
- Know how plugins are discovered, loaded, and secured via ARIA Shield
- Understand the 8 Pillars of Plugin Excellence
- Can use CSS variables for theme-aware styling
- Know the Plugin SDK namespaces and key methods
- Can create a basic plugin from scratch
- Can troubleshoot common plugin issues (403, integrity, theming)