DAY 12 WEEK 3: PLUGINS

Plugin Anatomy

Deep dive into plugin structure: directory layout, plugins.yaml schema, entry points, exports, bundle configuration, and component organization.

Every plugin is a small universeβ€”it has its own file structure, its own configuration, and its own rules of engagement with the platform. Understanding this anatomy is essential because configuration mistakes are the #1 cause of plugin failures. Today we dissect a plugin piece by piece.

πŸ“‹Learning Objectives

  • Understand the complete plugin directory structure and purpose of each file
  • Master the plugins.yaml manifest schema (the source of truth)
  • Learn about entry points, exports, and how the platform loads your code
  • Understand routes, widgets, and component organization patterns
  • Configure security settings: permissions, integrity, and PDP scoping
  • Set up the build process with esbuild

πŸ“Plugin Directory Structure

A plugin isn't just codeβ€”it's a self-contained package with configuration, source files, and build output. The structure matters because the platform expects specific files in specific locations.

File Locations

PurposePath
Tenant registry (Source of Truth)ServiceConfigs/BFF/config/plugins/index.yaml
Plugin manifests (per-plugin)ServiceConfigs/BFF/config/plugins/manifests/<plugin-id>.yaml
Plugin bundlesServiceConfigs/BFF/plugins/<pluginId>/<version>/index.esm.js
BFF container mounts/app/plugins and /app/config

Complete Plugin Structure

my-plugin/ β”œβ”€β”€ 1.0.0/ # Version directory β”‚ β”œβ”€β”€ package.json # NPM configuration β”‚ β”œβ”€β”€ tsconfig.json # TypeScript settings β”‚ β”œβ”€β”€ build.mjs # esbuild script β”‚ β”‚ β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”œβ”€β”€ index.tsx # Entry point (REQUIRED) β”‚ β”‚ β”œβ”€β”€ Dashboard.tsx # Main page component β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pages/ # Route-level components β”‚ β”‚ β”‚ β”œβ”€β”€ ListPage.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ DetailPage.tsx β”‚ β”‚ β”‚ └── SettingsPage.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ components/ # Reusable UI β”‚ β”‚ β”‚ β”œβ”€β”€ DataTable.tsx β”‚ β”‚ β”‚ └── ActionBar.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ hooks/ # Custom React hooks β”‚ β”‚ β”‚ └── useData.ts β”‚ β”‚ β”‚ β”‚ β”‚ └── services/ # API wrappers β”‚ β”‚ └── api.ts β”‚ β”‚ β”‚ └── dist/ # Build output β”‚ └── index.esm.js # Bundle (REQUIRED)
πŸ’‘ Version Directories

Each plugin version gets its own directory. This allows A/B testing, rollbacks, and multiple versions running simultaneously for different tenants.

πŸ“„Plugin Manifest Schema

Forget manifest.jsonβ€”in EmpowerNow, plugin configuration is split: index.yaml (tenant registry) + manifests/<plugin-id>.yaml (per-plugin config). This approach means you can manage all plugins across tenants, and the BFF hot-reloads changes without rebuilding.

Complete Schema Reference

tenants:
  # Use "*" for all tenants, or specific host for tenant isolation
  experience.self.empowernow.ai:
    - id: my-plugin                      # Unique identifier (required)
      title: "My Plugin"                 # Display name in navigation
      version: "1.0.0"                   # Semantic version
      enabled: true                      # Toggle without removing
      
      # 🎯 Host compatibility
      engine:
        experience: ">=1.0.0"            # Required platform version
      
      # πŸ” Security configuration (CRITICAL)
      authorization:
        pdp_application: "my-plugin"     # Policy namespace 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..."   # Optional: hash verification
      
      # πŸ”’ API permissions (allowlist)
      permissions:
        api:
          - method: GET
            path: /api/crud/tasks
          - method: POST
            path: /api/crud/execute
          - method: POST
            path: /access/v1/evaluation
        sse:                             # Server-Sent Events endpoints
          - /api/events/tasks/summary
      
      # πŸ—ΊοΈ Routes and widgets
      contributions:
        routes:
          - path: /my-plugin
            component: Dashboard
            title: "My Plugin"
            resource: plugin.route       # PDP resource hint
            action: view                 # PDP action hint
          - path: /my-plugin/settings
            component: SettingsPage
            title: "Settings"
        widgets:
          - slot: dashboard.main
            component: SummaryWidget
            resource: plugin.widget
            action: view

Key Fields Explained

FieldDescriptionRequired
idUnique plugin identifier (kebab-case)Yes
versionSemantic version stringYes
engine.experienceMinimum platform version (semver range)Yes
bundle.fileAbsolute path to ESM bundle in containerYes
bundle.integritySHA256 hash for tamper detectionNo*
authorization.pdp_applicationPolicy namespace for isolationYes
permissions.api[]Method + path allowlist for API callsYes
contributions.routes[]URL paths and component mappingsYes
⚠️ Security Best Practice

Always set bundle.integrity in production. Without it, a compromised bundle can be loaded without detection.

πŸšͺEntry Point (index.tsx)

The entry point is where your plugin meets the platform. It must export specific objects that the loader expectsβ€”routes and optionally widgets. Miss an export, and your plugin won't load.

Minimal Entry Point

// src/index.tsx - The simplest valid plugin
const React = (globalThis as any).React;

export function Dashboard() {
  return React.createElement('div', {
    style: { 
      padding: '24px',
      background: 'var(--card-bg)',
      color: 'var(--main-text)'
    }
  }, 'Hello from My Plugin!');
}

// Required export: routes object
export const routes = { Dashboard };

// Optional: widgets for dashboard slots
export const widgets = {};

// Default export for compatibility
export default { routes, widgets };

Production Entry Point

// src/index.tsx - Full-featured plugin
import { Protected, IfCan } from '@experience/plugin-auth';
import { CrudGrid } from '@empowernow/crud-ui';

const React = (globalThis as any).React;
const { usePlugin } = (globalThis as any).PluginSDK || {};

// Main page component
export function Dashboard() {
  const { sdk } = usePlugin();
  
  return React.createElement('div', { style: styles.container },
    React.createElement('h1', { style: styles.title }, 'My Plugin'),
    
    // Authorization-gated content
    React.createElement(IfCan, { resource: 'data', action: 'view' },
      React.createElement(CrudGrid, {
        sdk,
        from: 'system.object.get_all',
        columns: ['Name', 'Status', 'Created'],
        serverSidePagination: true,
        pageSize: 50
      })
    )
  );
}

// Detail page for individual items
export function DetailPage() {
  return React.createElement('div', null, 'Detail View');
}

// Dashboard widget (rendered in slots)
export function SummaryWidget() {
  return React.createElement('div', {
    style: {
      padding: '16px',
      background: 'var(--card-bg)',
      borderRadius: '8px'
    }
  }, 'Quick Summary Widget');
}

// Export mappings (component names must match plugins.yaml)
export const routes = { Dashboard, DetailPage };
export const widgets = { SummaryWidget };

export default { routes, widgets };
πŸ”— Component Name Matching

The component field in plugins.yaml must exactly match the exported function name. If you export Dashboard, the manifest must reference component: Dashboard.

πŸ”§Build Configuration (esbuild)

EmpowerNow plugins use esbuild for blazing-fast builds. Forget webpackβ€”esbuild builds in milliseconds, not minutes. The key is marking platform dependencies as external.

build.mjs

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

await esbuild.build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  format: 'esm',                  // ESM required
  platform: 'browser',
  target: 'es2020',
  outfile: 'dist/index.esm.js',
  
  // Critical: these are provided by the platform
  external: [
    'react',
    'react-dom',
    '@empowernow/ui',
    '@empowernow/crud-ui',
    '@experience/plugin-auth'
  ],
  
  // Production settings
  minify: true,
  sourcemap: true,
  
  // Tree-shaking for smaller bundles
  treeShaking: true
});

console.log('βœ… Build complete: dist/index.esm.js');

package.json

{
  "name": "my-plugin",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "node build.mjs",
    "watch": "node build.mjs --watch",
    "integrity": "powershell -Command \"(Get-FileHash dist/index.esm.js -Algorithm SHA256).Hash.ToLower()\""
  },
  "devDependencies": {
    "esbuild": "^0.21.0",
    "typescript": "^5.5.0"
  }
}
πŸ’‘ Generate Integrity Hash
# PowerShell
$hash = (Get-FileHash dist/index.esm.js -Algorithm SHA256).Hash.ToLower()
Write-Host "sha256:$hash"

# Then add to plugins.yaml
bundle:
  integrity: "sha256:$hash"

🧩Component Organization Patterns

As your plugin grows, organization becomes critical. Follow these patterns to keep your code maintainable and your bundle size small.

Pattern: Feature-Based Organization

# Group by feature, not file type
src/
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ users/
β”‚   β”‚   β”œβ”€β”€ UserList.tsx
β”‚   β”‚   β”œβ”€β”€ UserDetail.tsx
β”‚   β”‚   β”œβ”€β”€ userService.ts
β”‚   β”‚   └── useUsers.ts
β”‚   β”‚
β”‚   └── tasks/
β”‚       β”œβ”€β”€ TaskList.tsx
β”‚       β”œβ”€β”€ taskService.ts
β”‚       └── useTasks.ts
β”‚
β”œβ”€β”€ shared/
β”‚   β”œβ”€β”€ components/        # Cross-feature UI
β”‚   └── hooks/             # Cross-feature logic
β”‚
└── index.tsx              # Re-exports everything

Pattern: Lazy Loading for Large Plugins

// Only load DetailPage when navigated to
const DetailPage = React.lazy(() => import('./pages/DetailPage'));

export function Dashboard() {
  return React.createElement(React.Suspense, {
    fallback: React.createElement('div', null, 'Loading...')
  },
    React.createElement(DetailPage)
  );
}

βš™οΈPlugin SDK Reference

The Plugin SDK is your gateway to the platform. It provides authenticated API calls, authorization checks, CRUD operations, real-time updates, and telemetryβ€”all with automatic header stamping and fail-closed security.

SDK Namespaces

The SDK exposes five namespaces plus metadata:

interface PluginSDK {
  metadata: {
    pluginId: string;
    pdpApplication: string;           // From manifest.authorization.pdp_application
    baseContext: Record<string, any>; // From manifest.authorization.context
  };
  
  api: {                              // HTTP requests
    fetch: (path, init?) => Promise<Response>;
    useQuery: (key[], fn, opts?) => UseQueryResult;
  };
  
  sse: {                              // Server-Sent Events
    subscribe: (path, onMessage) => () => void;
    unsubscribeAll: () => void;
  };
  
  authz: {                            // Authorization checks
    evaluate: (req) => Promise<AuthZENResponse>;
    batchEvaluate: (evaluations[]) => Promise<AuthZENBatchResponse>;
  };
  
  crud: {                             // Orchestration Service
    use: (system, object, action, params?) => UseQueryResult;
    execute: (system, object, action, params?) => Promise<any>;
    run: (workflowName, data) => Promise<WorkflowResponse>;
  };
  
  telemetry: {                        // Custom event tracking
    track: (event, properties?) => void;
  };
  
  logger: Console;
}

api.fetch() β€” Authenticated Requests

// GET request with auto-stamped X-Plugin-Id header
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' })
});

authz.evaluate() β€” Authorization Checks

// Check if user can perform action on resource
const result = await sdk.authz.evaluate({
  resource: { type: 'workflow', id: workflowId },
  action: { name: 'start' },
  context: { reason: 'manual_launch' }
});

if (result.decision === true) {
  // User is authorized
}

authz.batchEvaluate() β€” Bulk Permissions (3x Faster!)

// Check multiple permissions in single API call
const results = await sdk.authz.batchEvaluate([
  { resource: { type: 'user', id: userId }, action: { name: 'edit' } },
  { resource: { type: 'user', id: userId }, action: { name: 'delete' } },
  { resource: { type: 'user', id: userId }, action: { name: 'reset-password' } }
]);

const permissions = {
  canEdit: results.decisions[0]?.decision === 'Permit',
  canDelete: results.decisions[1]?.decision === 'Permit',
  canReset: results.decisions[2]?.decision === 'Permit'
};

crud.use() β€” Data Fetching Hook

// React hook for CRUD data with automatic caching
const { data, isLoading, error, refetch } = sdk.crud.use(
  'av_openldap',          // system
  'account',              // object type
  'get_all_users',        // action
  { page_size: 50 }       // params
);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return data.map(user => <li key={user.id}>{user.name}</li>);

sse.subscribe() β€” Real-Time Updates

// Subscribe to server-sent events
useEffect(() => {
  const unsubscribe = sdk.sse.subscribe('/events/tasks/summary', data => {
    setCount(data.pending_count);
  });
  
  return unsubscribe;  // Cleanup on unmount
}, []);
πŸ”’ Security Model

Every SDK request includes X-Plugin-Id header. The BFF validates against the plugin's allowlist and rejects mismatches with 403 X-Allowlist-Violation: 1.

πŸ”Authorization Patterns

Never show UI without checking authorization first. The SDK provides components that handle authorization automaticallyβ€”no manual checks, no loading states to manage.

Protected Components

// ❌ BAD: Manual auth check (25 lines of boilerplate)
const [canDelete, setCanDelete] = useState(false);
const [loading, setLoading] = useState(true);

useEffect(() => {
  (async () => {
    try {
      const result = await sdk.authz.evaluate({...});
      setCanDelete(result.decision === 'Permit');
    } finally {
      setLoading(false);
    }
  })();
}, []);

if (loading) return <button disabled>Loading...</button>;
if (!canDelete) return null;
return <button onClick={handleDelete}>Delete</button>;

// βœ… GOOD: SDK handles everything (1 line!)
<Protected.Button resource="user" action="delete" onClick={handleDelete}>
  Delete
</Protected.Button>

Conditional Sections with IfCan

// Show entire section only if authorized
<IfCan resource="admin" action="view">
  <AdminPanel />
</IfCan>

// Multiple permissions (any match)
const { canAny } = usePluginAuth();

if (canAny([
  ['user', 'edit'],
  ['user', 'delete']
])) {
  return <UserActionsToolbar />;
}

Programmatic Checks

const { can, canAll, canAny } = usePluginAuth();

// Single permission
if (can('report', 'export')) {
  showExportButton = true;
}

// Multiple permissions (all required)
if (canAll([
  ['budget', 'view'],
  ['budget', 'approve']
])) {
  showApprovalUI = true;
}
πŸ’‘ What Protected.Button Provides
  • βœ… Authorization check (auto-hides if denied)
  • βœ… Loading state with spinner
  • βœ… Error handling with toast notifications
  • βœ… Success feedback
  • βœ… ARIA accessibility labels
  • βœ… Hover animations

πŸ›‘οΈAPI Permissions & Enforcement

Plugins operate in a sandboxed environment. Every API call is validated against the plugin's allowlist defined in plugins.yaml. This prevents plugins from accessing endpoints they weren't designed for.

How Enforcement Works

  1. Request: Plugin makes API call via SDK
  2. Header Stamp: SDK adds X-Plugin-Id: my-plugin
  3. Validation: BFF checks method + path against permissions.api[]
  4. Response: If allowed, request proceeds; if denied, 403

Permission Configuration

permissions:
  api:
    # Exact path matching
    - method: GET
      path: /api/crud/tasks
    
    # Path templates with wildcards
    - method: GET
      path: /api/crud/tasks/{id}
    
    # PDP authorization endpoint
    - method: POST
      path: /access/v1/evaluation
    
  sse:
    # Server-Sent Events endpoints
    - /api/events/tasks/summary
    - /crud/events/workflow/status

BFF Routes Reference

RoutePurposeAuth
GET /api/plugins/manifestsList all plugin manifestsSession required
GET /api/plugins/bundle?id=...Serve plugin bundleSession required

Enforcement Responses

ScenarioResponseHeader
Not in allowlist403 ForbiddenX-Allowlist-Violation: 1
Integrity mismatch409 ConflictX-Integrity-Error: 1
No session401 Unauthorizedβ€”

πŸ“ŠCRUD Integration

The CRUD SDK provides an ultra-minimal API for interacting with the Orchestration Service. Use pre-built components like CrudGrid for data tables and CrudForm for workflows.

CrudGrid β€” Data Table with Everything Built-in

<CrudGrid
  sdk={sdk}
  from="system.object.get_all"
  columns={['Name', 'Email', 'Status']}
  serverSidePagination={true}   // Handles 10,000+ items
  pageSize={50}
  enableSelection={true}
  multiSelect={true}
  rowActions={[
    'View: view',               // Format: "Label: action"
    'Edit: edit',
    'Delete: delete'
  ]}
  onRowClick={(row) => handleRowClick(row)}
  onSelectionChange={(selected) => setSelected(selected)}
/>

What CrudGrid Provides

  • βœ… Server-side pagination (handles 10,000+ items)
  • βœ… Sorting and filtering
  • βœ… Row selection (single/multi)
  • βœ… Row actions with authorization checks
  • βœ… Loading states and error handling
  • βœ… Empty states
  • βœ… Responsive design

Execute Workflows

// Run a workflow
const result = await sdk.crud.run('av_openldap_create_user', {
  FirstName: 'John',
  LastName: 'Doe',
  Email: 'john.doe@example.com'
});

console.log('User created:', result.workflow_id);

// Monitor workflow status
const status = await sdk.crud.getWorkflowStatus(result.workflow_id);
console.log('Status:', status.status);  // 'running', 'completed', 'failed'

Subscribe to Workflow Updates

const unsubscribe = sdk.crud.subscribeWorkflowStatus(executionId, (update) => {
  setStatus(update);
  if (update.status === 'completed') {
    console.log('Workflow completed:', update.result);
    unsubscribe();
  }
});

// Requires SSE permission:
// sse:
//   - /crud/events/workflow/status

πŸ”¬Hands-on Labs

Time to dissect a real plugin. These labs walk you through analyzing existing plugins and creating your own with proper structure.
LAB 1 Analyze plugins.yaml Structure ⏱️ 5 minutes
1

Open 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

# View a plugin manifest (e.g. it-shop)
cat <YourRepoRoot>/ServiceConfigs/BFF/config/plugins/manifests/it-shop.yaml
2

For each plugin, identify:

  • The pdp_application value
  • How many API endpoints are in the permissions allowlist
  • Which routes are contributed
πŸ€” Self-Check
  • Why does each plugin have its own pdp_application?
  • What happens if an API call is made to an endpoint not in permissions.api?
LAB 2 Create Plugin Skeleton ⏱️ 10 minutes
1

Create directory structure (replace <YourRepoRoot> with your project root):

mkdir -p <YourRepoRoot>/plugins/anatomy-lab/1.0.0/src
cd <YourRepoRoot>/plugins/anatomy-lab/1.0.0

Windows: use backslashes in path if in PowerShell; or use Git Bash/WSL for the same commands.

2

Create package.json with build scripts

3

Create src/index.tsx with exports:

const React = (globalThis as any).React;

export function Dashboard() {
  return React.createElement('div', {
    style: {
      padding: '24px',
      background: 'var(--card-bg)',
      borderRadius: '12px'
    }
  },
    React.createElement('h1', { 
      style: { color: 'var(--main-text)' } 
    }, 'πŸ”¬ Anatomy Lab Plugin')
  );
}

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

Build and generate integrity hash:

npm install
npm run build
npm run integrity
βœ… Expected Result

You should have dist/index.esm.js and a SHA256 hash ready for plugins.yaml.

LAB 3 Test SDK Integration ⏱️ 15 minutes
1

Add SDK usage to your plugin:

const React = (globalThis as any).React;
const { useState, useEffect } = React;
const sdk = (globalThis as any).pluginSdk;

export function Dashboard() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    (async () => {
      try {
        // Use SDK to fetch data
        const response = await sdk.api.fetch('/api/health');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    })();
  }, []);
  
  if (loading) return React.createElement('div', null, 'Loading...');
  if (error) return React.createElement('div', null, 'Error: ' + error);
  
  return React.createElement('div', { style: { padding: '24px' } },
    React.createElement('h1', null, 'SDK Lab'),
    React.createElement('pre', null, JSON.stringify(data, null, 2))
  );
}
2

Add the required API permission to plugins.yaml:

permissions:
  api:
    - method: GET
      path: /api/health
3

Rebuild and test in browser:

npm run build
# Restart BFF or wait for hot-reload
βœ… Expected Result

Your plugin should display the health check response. Check browser DevTools Network tab to see the X-Plugin-Id header.

πŸ€” Self-Check
  • What happens if you call an endpoint not in permissions.api?
  • Can you see the X-Plugin-Id header in DevTools?
  • What's the difference between sdk.api.fetch() and native fetch()?
LAB 4 Implement Authorization Check ⏱️ 10 minutes
1

Add an authorization-gated button:

export function Dashboard() {
  const [canDelete, setCanDelete] = useState(false);
  const [checking, setChecking] = useState(true);
  
  useEffect(() => {
    (async () => {
      try {
        const result = await sdk.authz.evaluate({
          resource: { type: 'task', id: 'demo-task' },
          action: { name: 'delete' }
        });
        setCanDelete(result.decision === true);
      } finally {
        setChecking(false);
      }
    })();
  }, []);
  
  return React.createElement('div', { style: { padding: '24px' } },
    React.createElement('h1', null, 'Authorization Lab'),
    
    checking 
      ? React.createElement('p', null, 'Checking permissions...')
      : canDelete
        ? React.createElement('button', {
            onClick: () => alert('Delete clicked!'),
            style: { background: '#ef4444', color: 'white', padding: '8px 16px' }
          }, 'Delete Task')
        : React.createElement('p', { style: { color: '#6b7280' } }, 
            'You do not have permission to delete.')
  );
}
2

Add PDP permission to plugins.yaml:

permissions:
  api:
    - method: POST
      path: /access/v1/evaluation
βœ… Expected Result

The plugin should check permissions and conditionally render the delete button based on PDP response.

πŸ”§Troubleshooting Plugin Anatomy Issues

πŸ” Component Not Found Case #1
πŸ“‹ Symptom

Route renders blank. Console shows "Component 'Dashboard' not found in plugin exports."

πŸ” Investigation

The component name in plugins.yaml doesn't match the exported function name.

βœ… Fix

Ensure exact match between export and manifest:

// index.tsx
export function Dashboard() { ... }  // Function name

# plugins.yaml
component: Dashboard  # Must match exactly
βš™οΈ Missing window.React Case #2
πŸ“‹ Symptom

Plugin crashes with "Cannot read property 'createElement' of undefined."

πŸ” Investigation

React wasn't marked as external in esbuild, so the plugin bundled its own copy which conflicts with the platform's React.

βœ… Fix
// build.mjs
external: ['react', 'react-dom']  // Mark as external

// index.tsx
const React = (globalThis as any).React;  // Use platform's React
πŸ“¦ Bundle Not Loading Case #3
πŸ“‹ Symptom

Network tab shows 404 for plugin bundle. "Plugin failed to load."

πŸ” Investigation

The bundle.file path doesn't match where the file was actually copied.

βœ… Fix
# Verify file exists at expected path (replace <YourRepoRoot> with your project root)
# Windows
dir <YourRepoRoot>\ServiceConfigs\BFF\plugins\my-plugin\1.0.0\

# macOS/Linux
ls <YourRepoRoot>/ServiceConfigs/BFF/plugins/my-plugin/1.0.0/

# Update plugins.yaml to match
bundle:
  file: "/app/plugins/my-plugin/1.0.0/index.esm.js"
🚫 API Call Blocked (403) Case #4
πŸ“‹ Symptom

API calls return 403 Forbidden. Response includes header X-Allowlist-Violation: 1.

πŸ” Investigation

The endpoint is not in the plugin's permissions.api allowlist, or the method doesn't match.

βœ… Fix
# Check what you're calling
# sdk.api.fetch('/api/crud/tasks', { method: 'DELETE' })

# Add to plugins.yaml
permissions:
  api:
    - method: DELETE
      path: /api/crud/tasks/{id}
πŸ”‘ Integrity Check Failed Case #5
πŸ“‹ Symptom

Bundle request returns 409 Conflict with X-Integrity-Error: 1.

πŸ” Investigation

The bundle.integrity hash in plugins.yaml doesn't match the actual file hash. This happens when:

  • You rebuilt the bundle but didn't update the hash
  • The file was modified after hash was generated
  • Wrong file was deployed
βœ… Fix
# Regenerate the hash
$hash = (Get-FileHash dist/index.esm.js -Algorithm SHA256).Hash.ToLower()
Write-Host "sha256:$hash"

# Update plugins.yaml
bundle:
  integrity: "sha256:$hash"
🎨 Theme Variables Not Working Case #6
πŸ“‹ Symptom

Plugin looks fine in development but colors are wrong in production. CSS variables like var(--card-bg) aren't resolving.

πŸ” Investigation

You're using custom CSS variable names instead of the platform's standard variables, or styles are being overridden.

βœ… Fix
// Use platform's CSS variables
style: {
  background: 'var(--card-bg)',      // βœ“ Platform variable
  color: 'var(--main-text)',         // βœ“ Platform variable
  border: '1px solid var(--border)', // βœ“ Platform variable
  
  // NOT these custom ones:
  // background: 'var(--my-custom-bg)',  // βœ— Won't work
}

❓Knowledge Check

1 What file serves as the Source of Truth for plugin configuration? β–Ό

ServiceConfigs/BFF/config/plugins/index.yaml (tenant registry) and ServiceConfigs/BFF/config/plugins/manifests/<plugin-id>.yaml (per-plugin manifest) β€” not a manifest.json in the plugin directory. This split approach allows managing all plugins from a centralized config directory.

2 What must the entry point (index.tsx) export? β–Ό

At minimum: export const routes = { ComponentName }. Optionally also export const widgets = {} and export default { routes, widgets }.

3 Why must React be marked as external in esbuild? β–Ό

The platform provides React via window.React. If your plugin bundles its own copy, you get two React instances which causes hooks to break and components to fail.

4 What does bundle.integrity do and why is it important? β–Ό

It's a SHA256 hash of the bundle file. The BFF verifies this hash before serving the bundle, preventing:

  • Tampered or malicious bundles
  • Accidental deployment of wrong version
  • Man-in-the-middle attacks
5 What's the relationship between component names in plugins.yaml and exports? β–Ό

They must match exactly. If plugins.yaml says component: Dashboard, your entry point must have export function Dashboard() or export const routes = { Dashboard }.

6 Where are plugin bundles physically stored? β–Ό

ServiceConfigs/BFF/plugins/<pluginId>/<version>/index.esm.js
This gets mounted to /app/plugins in the BFF container.

7 What are the 5 SDK namespaces and what does each do? β–Ό
  • api: HTTP requests with auto-stamped headers (fetch, useQuery)
  • sse: Server-Sent Events for real-time updates (subscribe, unsubscribeAll)
  • authz: PDP authorization checks (evaluate, batchEvaluate)
  • crud: Orchestration Service integration (use, execute, run)
  • telemetry: Custom event tracking (track)
8 What's the benefit of batchEvaluate() over multiple evaluate() calls? β–Ό

batchEvaluate() sends multiple permission checks in a single API callβ€”3x faster than individual calls.

Use case: Table rows with multiple actions per row. Instead of N Γ— 3 API calls, you make 1 batch call.

9 What happens when an API call violates the permissions allowlist? β–Ό

The BFF returns 403 Forbidden with header X-Allowlist-Violation: 1.

Fix: Add the endpoint to permissions.api[] in plugins.yaml with the correct method and path.

10 What is pdp_application in the manifest? β–Ό

pdp_application is the policy namespace for the plugin. It isolates the plugin's authorization policies from other plugins.

When sdk.authz.evaluate() is called, the SDK automatically adds this to the request context so PDP evaluates against the correct policy namespace.

11 How do Protected components differ from manual authorization checks? β–Ό

Protected.Button and similar components handle everything automatically:

  • Authorization check (auto-hides if denied)
  • Loading spinner while checking
  • Error handling with toast notifications
  • Success feedback
  • ARIA accessibility labels

Manual checks require 25+ lines of boilerplate; Protected components are 1 line.

πŸ“Day 12 Checkpoint

  • Understand the complete plugin directory structure
  • Can read and write valid plugins.yaml entries
  • Know what exports the entry point must provide
  • Understand esbuild configuration and external dependencies
  • Can generate and verify bundle integrity hashes
  • Know how component names map between manifest and exports
  • Understand all 5 Plugin SDK namespaces (api, sse, authz, crud, telemetry)
  • Can use Protected components for authorization-gated UI
  • Understand API permissions and allowlist enforcement
  • Can use batchEvaluate() for efficient permission checks
  • Know how to integrate with CrudGrid and CRUD operations
  • Can troubleshoot common anatomy and SDK issues