Plugin System Overview
This document explains the design philosophy, architecture, and key concepts of the OpenGPEX plugin system. Whether you want to build a custom tool or understand how the editor is structured, start here.
Design Philosophy
OpenGPEX strictly follows Inversion of Control (IoC) and the Open-Closed Principle to achieve complete decoupling between the core engine framework (Workspace) and business plugins.
┌─────────────────────────────────────────────────────────┐
│ Core Engine (Workspace) │
│ │
│ Registry ──── auto-discovers ────► Plugin A │
│ ────► Plugin B │
│ ────► Plugin C │
│ │
│ Workspace ── renders via Slots ── Registry │
└─────────────────────────────────────────────────────────┘
The core never imports plugins directly. Instead, plugins register themselves into a metadata-driven registry, and the workspace dynamically renders them into physical slots.
Dual-Track Loading Architecture
OpenGPEX uses a dual-track plugin loading system:
| Track | Method | Use Case |
|---|---|---|
| Track 1: Static | Source compiled into the Next.js bundle | Official built-in plugins, local development |
| Track 2: Dynamic | ESM ZIP loaded at runtime via import() |
Production hot-plug install, user uploads |
┌─────────────────────────────┬────────────────────────────────┐
│ Track 1: Static Compile │ Track 2: Dynamic Runtime │
│ │ │
│ plugins/base/ → bundle │ data/plugins/user/ → ESM load │
│ plugins/community/ → bundle │ /api/plugins/serve (proxy) │
│ plugins/user/ → bundle │ /api/plugins/upload (ingest) │
│ │ │
│ registry.ts (generated) │ /api/plugins/list (discovery) │
└─────────────────────────────┴────────────────────────────────┘
Dual-Registry System
The system maintains two separate registries:
core/plugin/registry.ts— All officialbaseandcommunityplugins, generated bysync-plugins.mjsplugins/registry-user.ts— All localuserplugins, generated byscan-plugins.mjs
The core loader usePluginInit loads both registries in sequence ("core first, user second") and hands them to PluginService for unified lifecycle management.
Trust Model (sourceType)
Every plugin carries a trust level:
| sourceType | Meaning | Install Method | Removable | Disablable |
|---|---|---|---|---|
base |
Official core features | Compiled into bundle | ❌ | ❌ |
community |
Verified third-party, pre-installed | Compiled into bundle | ❌ | ✅ |
user |
User-installed | ZIP upload / Hub download | ✅ | ✅ |
📌 Users can never elevate a plugin to
communitystatus at runtime. Only plugins shipped with the release qualify.
UID Namespace System
Every plugin, command, and signal receives a globally unique identifier (UID):
| Entity | Formula | Example |
|---|---|---|
| Plugin | author + "." + manifest.id |
opengpex.drawers.adjustment |
| Command | plugin.uid + "." + cmd.id |
opengpex.drawers.adjustment.cmd.reset |
| Signal | plugin.uid + "." + sig.id |
opengpex.drawers.adjustment.signal.active_tab |
This ensures zero naming collisions even when multiple plugins declare commands with the same local name.
Scoped Auto-Prefix
When a plugin calls actions.executeCommand(id) from within its own context:
- If
idis found directly in the global command registry → execute as-is (absolute path) - Otherwise → auto-prefix with
${pluginScope.uid}.${id}(relative path)
This means plugins can use short IDs internally (e.g., 'cmd.apply_color') without worrying about namespace collisions.
Plugin Metadata Contract
Every plugin exports a plugin object implementing the EditorPlugin interface:
import * as P from './protocols';
export const plugin: EditorPlugin = {
// 1. Identity
manifest: {
id: P.PLUGIN_ID, // e.g. 'drawers.my_tool'
displayName: 'My Tool',
version: '1.0.0',
description: 'A custom drawing tool',
category: 'drawers',
author: P.PLUGIN_AUTHOR, // e.g. 'my-username' — REQUIRED for UID
requirements: {
coreVersion: '>=1.0.0', // minimum engine version
auth: 'none',
},
},
// 2. Layout
slot: 'SIDE_BAR',
show: 'frame-required',
icon: <Palette size={20} />,
order: 400,
// 3. Implementation
component: MyToolComponent,
initialConfig: { mode: 'normal' },
// 4. Capabilities
commands: [/* ... */],
signals: [/* ... */],
interactions: [/* ... */],
contributions: [/* ... */],
// 5. Lifecycle
onInit: (ctx) => { /* setup */ },
onDestroy: (ctx) => { /* cleanup */ },
};
⚠️ Both
manifest.idandmanifest.authorare required. The system computes UID asauthor + "." + id. Missing either field will cause registration to fail.
Slot System
Plugins render into physical slots — predefined UI regions in the workspace:
| Slot | Location | Plugin Suffix Convention |
|---|---|---|
SIDE_BAR |
Sidebar drawers (left & right sides) | XxxDrawer, XxxPanel |
OPTION_BAR |
Top strip below the header | XxxOptions |
DOCK |
Collapsible bottom panel area | TabDock |
TL / TR / BL / BR |
Anchored HUD corners on the viewport | XxxHUD, XxxWidget |
VIEWPORT_OVERLAY |
Over the canvas (pointer-interactive) | XxxOverlay |
STAGE_OVERLAY |
Stage-level layers (non-interactive) | XxxOverlay |
STAGE_GIZMOS |
Interactive transform & crop tools | SmartGuides |
ROOT_OVERLAY |
App-level absolute layers | XxxPopup, XxxOverlay |
TOOL_MENU |
Left vertical toolbar options | XxxTool |
HIDDEN |
Headless background loops | TimeTraveler, XxxService |
Plugin Lifecycle
Install/Enable → onInit(ctx) → Active (renders in slot) → Disable → onDestroy(ctx)
- Registration — Plugin metadata is scanned and added to the registry
- Initialization —
onInitfires with fullEditorContextValue - Active — Component renders in its assigned slot, commands/signals are live
- Destruction —
onDestroyfires for cleanup when disabled or uninstalled
Lifecycle Rules
onInit/onDestroyshould be synchronous and lightweight (fire-and-forget any async work)onDestroymust be idempotent — multiple calls must not throw- No DOM manipulation or React re-renders in lifecycle hooks (they execute outside the React tree)
- Extract lifecycle logic into a separate
lifecycle.tsfile
The 5-File Pattern
Every plugin follows a standard file structure:
plugins/base/[category]/[PluginName]/
├── index.tsx # Plugin entry — exports EditorPlugin object
├── protocols.ts # Constants (PLUGIN_ID, PLUGIN_AUTHOR, command IDs, signal IDs, config interfaces)
├── commands.ts # Command executors (business logic)
├── hooks.ts # Custom hooks (bridge UI ↔ commands/signals)
└── components.tsx # React UI components (pure presentation)
Optional additional files:
lifecycle.ts—onInit/onDestroyimplementations (when needed)worker/— Plugin-private Web Worker directory (for heavy computation)
Next Steps
- Your First Plugin — Build a working plugin from scratch
- API Reference — Full hook and command documentation
- Slots & UI — Detailed slot types and Z-Index protocol
- Signals & Cross-Plugin Communication — Reactive state and inter-plugin messaging
Last updated: 2026-07-30