DAY 15 WEEK 3: PLUGINS

Plugin Deployment & Testing

Deploy your plugin to the Experience Platform, verify integrity, set up debugging workflows, and complete the Week 3 assessment.

You've built an enterprise-grade plugin. Now it's time to ship it. Today covers the full deployment pipeline: building the bundle, generating integrity hashes, deploying to the platform, and setting up debugging workflows for ongoing development.

📋Learning Objectives

  • Build production-ready ESM bundles with esbuild
  • Generate and verify SHA-256 integrity hashes
  • Deploy plugins to the Experience Platform
  • Configure local development with hot reload
  • Debug plugins using browser DevTools
  • Troubleshoot common deployment issues
STEP 1 Production Build with esbuild ⏱️ 10 minutes

Build Script Review

Let's review the esbuild configuration from Day 13:

// build.mjs
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outfile: 'dist/index.esm.js',
  format: 'esm',
  minify: true,              // Minify for production
  sourcemap: true,           // Generate sourcemaps for debugging
  target: ['es2020'],
  external: [
    'react',
    'react-dom',
    '@empowernow/ui',
    '@empowernow/crud-ui',
    '@experience/plugin-auth'
  ],
  define: {
    'process.env.NODE_ENV': '"production"'
  }
});

Run the Build

# Navigate to plugin directory (replace <YourRepoRoot> with your project root)
# Windows
cd <YourRepoRoot>\experience\plugins\role-viewer\1.0.0

# macOS/Linux
cd <YourRepoRoot>/ServiceConfigs/BFF/plugins/role-viewer/1.0.0

# Install dependencies (if not done)
npm install

# Run the build
npm run build

# Verify output
dir dist   # Windows — or: ls dist (macOS/Linux)

# Expected output:
#   index.esm.js      (bundled code, ~15-50KB)
#   index.esm.js.map  (sourcemap for debugging)

Build Output Analysis

# Check bundle size
(Get-Item dist\index.esm.js).Length / 1KB

# If bundle is too large (>100KB), check for:
# - Accidentally bundled external dependencies
# - Large libraries that should be external
# - Development-only code not tree-shaken
💡 Bundle Size Guidelines

<50KB: Excellent - fast load times

50-100KB: Acceptable for feature-rich plugins

>100KB: Review externals config - something may be bundled incorrectly

STEP 2 Generate Integrity Hash ⏱️ 10 minutes
The platform uses Subresource Integrity (SRI) to verify plugin bundles haven't been tampered with. Every deploy requires updating the integrity hash in the plugin manifest.

Add Integrity Script to package.json

{
  "scripts": {
    "build": "node build.mjs",
    "integrity": "node -e \"const crypto = require('crypto'); const fs = require('fs'); const hash = crypto.createHash('sha256').update(fs.readFileSync('dist/index.esm.js')).digest('base64'); console.log('sha256-' + hash);\"",
    "deploy": "npm run build && npm run integrity"
  }
}

Generate the Hash

# Run integrity script
npm run integrity

# Output example:
# sha256-abc123def456ghi789jkl012mno345pqr678stu901vwx234=

Update Plugin Manifest

# ServiceConfigs/BFF/config/plugins/manifests/role-viewer.yaml
id: role-viewer
title: "Role Viewer"
version: 1.0.0

bundle:
  file: index.esm.js
  # Update this after every build!
  integrity: sha256-abc123def456ghi789jkl012mno345pqr678stu901vwx234=

# ... rest of config
⚠️ Critical: Always Update Integrity

If you rebuild and deploy without updating the integrity hash, the platform will reject the bundle with a security error. Make it part of your deploy workflow.

Automated Deploy Script

// deploy.mjs - Full deploy automation
import * as esbuild from 'esbuild';
import { createHash } from 'crypto';
import { readFileSync, writeFileSync } from 'fs';
import { parse, stringify } from 'yaml';

// 1. Build
await esbuild.build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outfile: 'dist/index.esm.js',
  format: 'esm',
  minify: true,
  sourcemap: true,
  target: ['es2020'],
  external: ['react', 'react-dom', '@empowernow/ui', '@empowernow/crud-ui', '@experience/plugin-auth']
});

// 2. Generate integrity hash
const bundle = readFileSync('dist/index.esm.js');
const hash = createHash('sha256').update(bundle).digest('base64');
const integrity = `sha256-${hash}`;

// 3. Update plugins.yaml
const yamlContent = readFileSync('plugins.yaml', 'utf8');
const config = parse(yamlContent);
config.bundle.integrity = integrity;
writeFileSync('plugins.yaml', stringify(config));

console.log('✅ Build complete');
console.log(`📦 Bundle size: ${(bundle.length / 1024).toFixed(1)}KB`);
console.log(`🔒 Integrity: ${integrity}`);
STEP 3 Deploy to Experience Platform ⏱️ 15 minutes

Deployment Structure

The Experience Platform serves plugins from a specific directory structure:

experience/
└── plugins/
    └── role-viewer/
        └── 1.0.0/
            ├── plugins.yaml      # Plugin manifest
            └── dist/
                ├── index.esm.js  # Bundle
                └── index.esm.js.map

Copy Files to Deploy Location

# Windows (PowerShell) — replace <YourRepoRoot> with your project root
$target = "<YourRepoRoot>\experience\plugins\role-viewer\1.0.0"
New-Item -ItemType Directory -Force -Path "$target\dist"
Copy-Item "dist\index.esm.js" "$target\dist\"
Copy-Item "dist\index.esm.js.map" "$target\dist\"
Copy-Item "plugins.yaml" "$target\"
dir $target -Recurse

# macOS/Linux
cd <YourRepoRoot>/ServiceConfigs/BFF/plugins/role-viewer/1.0.0
mkdir -p dist
cp dist/index.esm.js dist/index.esm.js.map dist/
cp plugins.yaml .
ls -R

Register Plugin (First Time Only)

If this is a new plugin, add it to the platform's plugin registry:

# ServiceConfigs/BFF/config/plugins/index.yaml (tenant registry)
# Add the plugin id to the enabled list

# ServiceConfigs/BFF/config/plugins/manifests/role-viewer.yaml (per-plugin manifest)
id: role-viewer
title: "Role Viewer"
version: 1.0.0
enabled: true
    
  # ... other plugins

Verify Deployment

# 1. Check bundle is accessible
curl -s "https://experience.self.empowernow.ai/plugins/role-viewer/1.0.0/dist/index.esm.js" | head -c 200

# 2. Check plugins.yaml is readable
curl -s "https://experience.self.empowernow.ai/plugins/role-viewer/1.0.0/plugins.yaml"

# 3. Navigate to plugin route in browser
# https://experience.self.empowernow.ai/role-viewer
STEP 4 Local Development Setup ⏱️ 15 minutes
Rebuilding and deploying for every change is slow. Let's set up local development with file watching for instant feedback.

Add Watch Mode to esbuild

// build.mjs - Add watch mode
import * as esbuild from 'esbuild';

const isWatch = process.argv.includes('--watch');

const buildOptions = {
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outfile: 'dist/index.esm.js',
  format: 'esm',
  minify: !isWatch,  // Don't minify in dev
  sourcemap: true,
  target: ['es2020'],
  external: ['react', 'react-dom', '@empowernow/ui', '@empowernow/crud-ui', '@experience/plugin-auth']
};

if (isWatch) {
  const ctx = await esbuild.context(buildOptions);
  await ctx.watch();
  console.log('👀 Watching for changes...');
} else {
  await esbuild.build(buildOptions);
  console.log('✅ Build complete');
}

Update package.json Scripts

{
  "scripts": {
    "dev": "node build.mjs --watch",
    "build": "node build.mjs",
    "integrity": "node -e \"...\"",
    "deploy": "npm run build && npm run integrity"
  }
}

Start Development Mode

# Start watching
npm run dev

# Output:
# 👀 Watching for changes...

# Now edit src/index.tsx - bundle rebuilds automatically!

Local Plugin Override (Optional)

For faster iteration, you can override the plugin URL to load from localhost:

// In browser console on Experience Platform
localStorage.setItem('plugin_override_role-viewer', 'http://localhost:8080/dist/index.esm.js');

// Serve locally with a simple HTTP server
npx serve dist -p 8080 --cors

// Clear override when done
localStorage.removeItem('plugin_override_role-viewer');
💡 Hot Reload Workflow
  1. Run npm run dev in plugin directory
  2. Run npx serve dist -p 8080 --cors in another terminal
  3. Set localStorage override in browser
  4. Edit code → save → refresh browser → see changes
STEP 5 Debugging with DevTools ⏱️ 15 minutes

Console Debugging

// Add strategic console logs
export function Dashboard() {
  console.log('[RoleViewer] Dashboard mounting');
  
  const fetchRoles = async () => {
    console.log('[RoleViewer] Fetching roles...');
    try {
      const response = await sdk.api.fetch('/api/membership/users/me/roles');
      console.log('[RoleViewer] Response status:', response.status);
      const data = await response.json();
      console.log('[RoleViewer] Roles received:', data);
    } catch (err) {
      console.error('[RoleViewer] Fetch error:', err);
    }
  };
}

Network Tab Investigation

Check What to Look For
Plugin bundle load 200 status, correct file size, no CORS errors
API calls Correct URL, auth headers present, response data
Authorization calls /access/v1/evaluation returns decision
SSE connections EventStream type, staying open, receiving events

Sourcemap Debugging

With sourcemaps enabled, you can debug original TypeScript code:

// 1. Open DevTools → Sources tab
// 2. Find your plugin under webpack:// or file://
// 3. Navigate to src/index.tsx
// 4. Click line number to set breakpoint
// 5. Refresh page → execution pauses at breakpoint

React DevTools

Install the React DevTools browser extension to inspect:

  • Component tree: Find your plugin components
  • Props: See what data components receive
  • State: Inspect useState/useReducer values
  • Hooks: See effect dependencies and values

Common Debug Patterns

// Debug authorization issues
const checkAuth = async () => {
  const request = {
    resource: { type: 'role-viewer.roles' },
    action: { name: 'view' }
  };
  console.log('[Auth] Request:', JSON.stringify(request, null, 2));
  
  const result = await sdk.authz.evaluate(request);
  console.log('[Auth] Result:', JSON.stringify(result, null, 2));
  
  return result.decision === true;
};

// Debug API response parsing
const fetchRoles = async () => {
  const response = await sdk.api.fetch('/api/membership/users/me/roles');
  const text = await response.text();  // Get raw text first
  console.log('[API] Raw response:', text);
  
  try {
    const data = JSON.parse(text);
    console.log('[API] Parsed data:', data);
    return data;
  } catch (e) {
    console.error('[API] Parse error - response was not JSON:', text);
    throw e;
  }
};

🔄Migrating React Apps to Plugins

Have an existing React application you want to integrate into the Experience Platform? Migration is the process of converting a standalone React app into a platform plugin. This section walks through the key structural changes and common pitfalls.

Structural Differences

Understanding what changes between a traditional React app and a plugin is the first step:

Aspect Traditional App Experience Plugin
Entry Point ReactDOM.render() export default manifest
Dependencies Bundled with app Externalized (host provides)
Routing React Router in app Host manages routes
Global State App-owned (Redux/Context) Plugin-scoped or shared
Styling Global CSS Scoped/CSS Modules
Build Output HTML + JS chunks Single ESM file

Directory Structure Comparison

# Traditional React App
my-react-app/
├── public/
│   ├── index.html        # Entry HTML (REMOVE)
│   └── favicon.ico
├── src/
│   ├── components/
│   ├── pages/
│   ├── App.tsx           # Root component (REFACTOR)
│   ├── App.css           # Global styles (SCOPE)
│   ├── index.tsx         # ReactDOM.render (REMOVE)
│   └── index.css         # Global CSS (AVOID)
├── package.json
└── vite.config.ts

# Experience Plugin
my-plugin/
└── 1.0.0/
    ├── src/
    │   ├── components/
    │   ├── index.tsx     # Plugin entry (exports manifest)
    │   └── styles.ts     # Scoped styles
    ├── dist/
    │   └── index.esm.js  # Single ESM bundle
    ├── plugins.yaml      # Plugin manifest
    ├── build.mjs
    └── package.json

Step-by-Step Migration

1. Remove Browser Entry Points

// ❌ BEFORE: src/index.tsx (Traditional App)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(<App />);

// ✅ AFTER: src/index.tsx (Plugin Entry)
const React = (globalThis as any).React;
import { Dashboard } from './components/Dashboard';
import { Settings } from './components/Settings';

export const routes = { Dashboard, Settings };
export const widgets = {};
export default { routes, widgets };

2. Externalize Dependencies

// ❌ BEFORE: package.json (Everything bundled)
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.20.0",
    "@mui/material": "^5.14.0"
  }
}

// ✅ AFTER: package.json (Externals as peerDeps)
{
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "esbuild": "^0.19.0",
    "typescript": "^5.3.0"
  }
}

3. Handle Global Styles

// ❌ BEFORE: Global CSS affects entire app
/* src/index.css */
* { margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; }

// ✅ AFTER: Scoped styles with CSS-in-JS
const styles = {
  container: {
    padding: '24px',
    background: 'var(--card-bg, #16161f)',
    borderRadius: '8px'
  },
  header: {
    fontSize: '24px',
    fontWeight: 600,
    marginBottom: '16px'
  }
};

// Or use CSS Modules
import styles from './Dashboard.module.css';

4. Migrate Routing

// ❌ BEFORE: React Router in App
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </BrowserRouter>
  );
}

// ✅ AFTER: Declare routes in plugins.yaml, host manages navigation
# plugins.yaml
contributions:
  routes:
    - path: /my-plugin/dashboard
      component: Dashboard
    - path: /my-plugin/settings
      component: Settings

// Components navigate using window.location or host's navigate
const handleNavigate = () => {
  window.location.href = '/my-plugin/settings';
};

5. Update API Calls

// ❌ BEFORE: Direct API calls
import axios from 'axios';

const API_BASE = 'https://api.myapp.com';

async function fetchData() {
  const response = await axios.get(`${API_BASE}/data`);
  return response.data;
}

// ✅ AFTER: Use Platform SDK (BFF handles auth, CSRF)
const sdk = (globalThis as any).pluginSdk;

async function fetchData() {
  const response = await sdk.api.fetch('/api/my-service/data');
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

6. Configure esbuild

// build.mjs - ESM plugin build
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outfile: 'dist/index.esm.js',
  format: 'esm',
  platform: 'browser',
  target: 'es2020',
  
  // CRITICAL: Externalize host-provided dependencies
  external: [
    'react',
    'react-dom',
    'react/jsx-runtime',
    '@empowernow/*'
  ],
  
  minify: true,
  sourcemap: true,
  
  define: {
    'process.env.NODE_ENV': '"production"'
  }
});

Migration Checklist

  • No ReactDOM.render() or createRoot() calls
  • All platform dependencies externalized in esbuild config
  • No global CSS (use CSS modules or styled-components)
  • Routes exported in plugin manifest (plugins.yaml)
  • Bundle size under 200KB (check with npm run build)
  • Integrity hash generated
  • TypeScript compiles without errors
  • No direct API calls (use Platform SDK via BFF)
  • Navigation uses platform's router or window.location
  • State is plugin-scoped (not global Redux)

Common Migration Pitfalls

⚠️ Watch Out For These
  1. Forgetting to externalize React: Results in 300KB+ bundles and duplicate React instances
  2. Using global CSS: Causes style conflicts with host and other plugins
  3. Direct DOM manipulation: Breaks in plugin isolation (no document.getElementById('root'))
  4. Hardcoded API URLs: Use platform's BFF proxy instead
  5. App-level routing: Let the host manage navigation via plugins.yaml
  6. Large asset bundles: Inline only small assets, use CDN for large ones
  7. Using import React: Must use globalThis.React
💡 Quick Win: Start Small

Don't migrate your entire app at once. Start with a single page/feature as a plugin, verify it works, then migrate the rest incrementally. This reduces risk and helps you learn the plugin patterns.

🔧Deployment Troubleshooting

Issue: "Integrity check failed"

# Symptom: Plugin fails to load with security error
# Cause: Hash in plugins.yaml doesn't match bundle

# Fix: Regenerate hash and update plugins.yaml
npm run integrity
# Copy output to plugins.yaml bundle.integrity field
# Redeploy plugins.yaml

Issue: "Component not found"

# Symptom: Route shows blank or error
# Cause: Export name doesn't match plugins.yaml

# Check plugins.yaml:
contributions:
  routes:
    - component: Dashboard  # Must match export name exactly

# Check index.tsx:
export const routes = { Dashboard };  # Must export this name

Issue: "403 Forbidden on API calls"

# Symptom: API calls fail with 403
# Cause: Missing permission in plugins.yaml

# Fix: Add the endpoint to permissions.api
permissions:
  api:
    - method: GET
      path: /api/membership/users/{id}/roles  # Add this

Issue: "CORS error in console"

# Symptom: Plugin loads but API fails with CORS
# Cause: Trying to call backend directly instead of through BFF

# Wrong:
fetch('https://membership.self.empowernow.ai/...')

# Correct:
sdk.api.fetch('/api/membership/...')  # Goes through BFF proxy

Issue: "globalThis.React is undefined"

# Symptom: TypeError when component renders
# Cause: Using import React instead of globalThis

# Wrong:
import React from 'react';

# Correct:
const React = (globalThis as any).React;

🏆Week 3 Assessment

Before moving to Week 4, verify you can complete these tasks:

✅ Plugin Structure

Create plugins.yaml with routes, permissions, PDP application

✅ Build System

Configure esbuild with externals, generate integrity hash

✅ React Components

Use globalThis.React, create functional components

✅ Plugin SDK

Use sdk.api.fetch(), sdk.authz.evaluate(), sdk.sse.subscribe()

✅ Authorization

Implement IfCan, batch permissions, fail-closed pattern

✅ Deployment

Build, hash, deploy, verify plugin works in platform

Self-Check Questions

  1. Why must React be accessed via globalThis.React instead of importing?
  2. What happens if you deploy without updating the integrity hash?
  3. How do you check multiple permissions efficiently?
  4. What's the difference between permissions.api and permissions.sse?
  5. Where do sourcemaps help during debugging?

Knowledge Check

1 What command generates the integrity hash?

npm run integrity which runs a Node script that:

  1. Reads the bundle file
  2. Computes SHA-256 hash
  3. Outputs sha256-{base64-hash}
2 Why use sourcemaps in production?

Sourcemaps let you debug minified code by mapping it back to original source. Without them, error stack traces show unintelligible minified code like a.b.c(). With sourcemaps, you see Dashboard.fetchRoles() at the correct line in your TypeScript.

3 What's the local dev workflow for fast iteration?
  1. npm run dev - Start esbuild in watch mode
  2. npx serve dist -p 8080 --cors - Serve bundle locally
  3. Set localStorage.plugin_override_role-viewer to localhost URL
  4. Edit → Save → Refresh → See changes
4 How do you verify a plugin deployed correctly?
  1. Curl the bundle URL - should return JS code, not 404
  2. Curl plugins.yaml - should return valid YAML
  3. Navigate to plugin route in browser - should render
  4. Check Network tab - no integrity or 403 errors
5 What does the external array in esbuild config do?

The external array tells esbuild not to bundle these dependencies. Instead, they're expected to be provided by the host platform at runtime. This keeps plugin bundles small and ensures all plugins share the same React instance.

external: ['react', 'react-dom', '@empowernow/ui']

📝Day 15 Checkpoint

  • Built production bundle with esbuild
  • Generated SHA-256 integrity hash
  • Updated plugins.yaml with new hash
  • Deployed plugin to Experience Platform
  • Set up local development with watch mode
  • Practiced debugging with DevTools
  • Understand React app to plugin migration steps
  • Completed Week 3 self-assessment
🎉 Week 3 Complete!

You've built, enhanced, and deployed a complete plugin. You understand authorization, real-time updates, and professional UX patterns. Week 4 covers advanced topics: security hardening, performance optimization, and plugin composition.