DAY 11 WEEK 3: PLUGINS

Experience Platform Architecture

Master the unified end-user portal: plugin architecture, excellence playbook, theming, SDK APIs, and creating plugins from scratch.

The Experience app isn't just another dashboard—it's a PDP-aware, plugin-extensible portal that dynamically enables features at runtime. Every button, every route, every widget is governed by authorization policies. Plugins don't just "add features"—they become first-class citizens with full access to the platform's security, theming, and observability stack. Today you'll learn how it all works, and build your first plugin.

📋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

Most enterprise portals are just SSO tile launchers. Experience is different—it's a unified, policy-governed portal where plugins share a common security context, theme system, and data layer. Think of it as an operating system for identity workflows.

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:

Experience Platform Architecture
flowchart TB subgraph Experience["🖥️ Experience Platform"] Shell["App Shell
(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
🔗 Service Endpoint

https://experience.self.empowernow.ai
All traffic routes through ARIA Shield—plugins never communicate directly with backend services.

📦Plugin Loading Mechanism

Plugins aren't compiled into the main bundle—they're loaded dynamically at runtime. This means you can deploy a new plugin without rebuilding the entire application. ARIA Shield validates each plugin bundle before loading, ensuring integrity and authorization.

Plugins are loaded dynamically at runtime using module federation:

Plugin Loading Flow
sequenceDiagram participant User participant Shell as App Shell participant Registry as Plugin Registry participant CDN as Plugin CDN participant Plugin User->>Shell: Navigate to /plugins/my-plugin Shell->>Registry: Fetch plugin manifest Registry-->>Shell: manifest.json Shell->>CDN: Load plugin bundle CDN-->>Shell: plugin.js Shell->>Plugin: Initialize & mount Plugin-->>User: Render UI

Loading Steps

  1. Discovery: Shell fetches available plugins from registry
  2. Manifest: Each plugin has a manifest.json with metadata
  3. Loading: Plugin bundle loaded via dynamic import
  4. Mounting: Plugin React component mounted in host container

🛣️Route Registration

Every plugin gets its own URL namespace, preventing conflicts. Routes are defined declaratively in the manifest—the platform handles the React Router integration automatically.

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"
    }
  ]
}
🔗 Route Prefix

All plugin routes are automatically prefixed with /plugins/{plugin-name}. So /roles becomes /plugins/role-viewer/roles.

📄Manifest Format (Plugin Config)

The manifest is your plugin's identity card—it tells the platform who you are, what permissions you need, and how to load you. In EmpowerNow, plugin configuration is split across two locations:
📂 Plugin Config Layout

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

Building a plugin that "works" is easy. Building a plugin that's commercial SaaS-quality—secure, accessible, performant, and delightful—requires following patterns that have been battle-tested across hundreds of enterprise deployments.
1 Authorization First

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>
2 Data Loading Excellence

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 }
);
3 Loading States Everywhere

Every async operation needs a loading state. Users should never see blank screens or wonder if something is happening.

4 Error Handling with Grace

Fail gracefully with actionable error messages. Never show raw stack traces or "Something went wrong."

5 Micro-Interactions

Hover effects, smooth transitions, and subtle animations make the difference between "functional" and "premium."

6 Keyboard Shortcuts

Power users expect shortcuts. Ctrl+N for create, Esc for close, Enter to submit.

7 Theming & Branding

Never hardcode colors. Always use CSS variables. Your plugin must work across all 9 themes.

8 Accessibility

ARIA labels, keyboard navigation, focus indicators. The SDK components have this built-in.

🎨The 9-Theme System

The Experience platform supports 9 distinct themes—5 dark, 4 light. Your plugin must work flawlessly on all of them. The secret? Never hardcode colors. Always use CSS variables.

Available Themes

🔵 NeonFlux
Cyan accent (default)
⚫ Modern Dark
Blue accent, minimal
🟣 Purple Power
Purple gradients
🟢 Forest Green
Nature-inspired
🟠 Sunset Orange
Energetic
⚪ Clean Light
Minimal light
💼 Financial Pro
Banking-friendly
🏥 Healthcare Blue
Medical, clean

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
⚠️ The Golden Rule

Never hardcode colors! background: '#1a1a1a' is invisible on dark themes. color: '#ffffff' is invisible on light themes. Always use CSS variables.

🔌Plugin SDK Reference

The SDK is your plugin's gateway to the platform. It handles authentication, authorization, API calls, SSE subscriptions, and telemetry—all with automatic header stamping and fail-closed security.

SDK Namespaces

NamespacePurposeKey Methods
sdk.apiHTTP requestsfetch(), useQuery()
sdk.authzAuthorizationevaluate(), batchEvaluate()
sdk.crudData operationsuse(), execute(), run()
sdk.sseReal-time updatessubscribe(), unsubscribeAll()
sdk.telemetryAnalyticstrack()

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

Time to build something real. These labs take you from exploring existing plugins to creating your own production-ready plugin in under 15 minutes.
LAB 1 Explore Existing Plugins ⏱️ 5 minutes

Let's examine how existing plugins are structured.

1

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/
2

List enabled plugins:

Look for the tenants section and identify which plugins are enabled: true.

3

Examine a plugin's bundle directory:

# Windows
dir <YourRepoRoot>\ServiceConfigs\BFF\plugins

# macOS/Linux
ls <YourRepoRoot>/ServiceConfigs/BFF/plugins
🤔 Self-Check
  • What pdp_application does each plugin use?
  • What API permissions does each plugin declare?
  • What routes does each plugin contribute?
LAB 2 Create a Plugin from Scratch ⏱️ 10 minutes

Build a production-ready plugin using the Excellence Playbook patterns.

1

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.

2

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"
  }
}
3

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 };
4

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/
5

Add to plugins.yaml and restart BFF

✅ Expected Result

Navigate to https://experience.self.empowernow.ai/training-plugin and see your plugin running with proper theming!

🔧Troubleshooting Plugin Issues

Plugin development has unique failure modes. Here are the most common issues and how to diagnose them.
🚫 403 Forbidden on API Calls Case #1
📋 Symptom

Plugin loads, but API calls return 403 Forbidden. Response has header X-Allowlist-Violation: 1.

🔍 Investigation
  • Check the permissions.api section in plugins.yaml
  • Verify the path matches exactly (including leading /api/)
  • Check the HTTP method (GET vs POST)
✅ Fix
permissions:
  api:
    - method: POST
      path: /api/crud/execute  # Add missing endpoint
🔒 Bundle Integrity Check Failed Case #2
📋 Symptom

Plugin fails to load. Console shows "Bundle integrity check failed."

🔍 Investigation

The SHA256 hash in plugins.yaml doesn't match the actual bundle file.

✅ Fix
# Regenerate hash (PowerShell)
$hash = (Get-FileHash dist/index.esm.js -Algorithm SHA256).Hash.ToLower()
Write-Host "sha256:$hash"

# Update plugins.yaml with new hash
🎨 Theme Switching Breaks UI Case #3
📋 Symptom

Plugin looks fine on dark theme, but text is invisible on light theme (white-on-white).

🔍 Investigation

Hardcoded colors in the plugin code.

✅ Fix
// ❌ BAD
color: '#ffffff'

// ✅ GOOD
color: 'var(--main-text)'

Knowledge Check

Test your understanding of the Experience platform before moving on to advanced plugin development.
1 What makes Experience different from typical portals like Okta/ForgeRock app launchers?

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.

2 What is ARIA Shield and why do plugins communicate through it?

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
3 What is the pdp_application field in the manifest?

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.

4 Why must plugins use CSS variables instead of hardcoded colors?

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.

5 What does the bundle.integrity field do?

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
6 What's the difference between Protected.Button and a regular button with manual auth checks?

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
7 How do you get real-time updates in a plugin?

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)