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)  │
└─────────────────────────────┴────────────────────────────────┘

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 community status 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.


Plugin Metadata Contract

Every plugin exports a plugin object implementing the EditorPlugin interface:

export const plugin: EditorPlugin = {
  // 1. Identity
  manifest: {
    id: 'drawers.my_tool',
    displayName: 'My Tool',
    version: '1.0.0',
    description: 'A custom drawing tool',
    category: 'drawers',
    author: 'opengpex',
  },

  // 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: [/* ... */],

  // 5. Lifecycle
  onInit: (ctx) => { /* setup */ },
  onDestroy: (ctx) => { /* cleanup */ },
};

Slot System

Plugins render into physical slots — predefined UI regions in the workspace:

Slot Location Plugin Suffix Convention
SIDE_BAR Left sidebar panels XxxDrawer, XxxPanel
OPTION_BAR Top options strip XxxOptions
VIEWPORT_OVERLAY Canvas overlay (handles, guides) XxxOverlay
STAGE_OVERLAY Stage-level overlay XxxOverlay
HIDDEN No UI, background service TimeTraveler, etc.
DOCK Bottom dock area TabDock

Plugin Lifecycle

Install/Enable → onInit(ctx) → Active (renders in slot) → Disable → onDestroy(ctx)
  1. Registration — Plugin metadata is scanned and added to the registry
  2. InitializationonInit fires with full editor context
  3. Active — Component renders in its assigned slot, commands/signals are live
  4. DestructiononDestroy fires for cleanup when disabled or uninstalled

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, command IDs, config interfaces)
├── commands.ts     # Command executors (business logic)
├── hooks.ts        # Custom hooks (bridge UI ↔ commands/signals)
└── components.tsx  # React UI components (pure presentation)

Next Steps


Last updated: 2026-06-14