Skip to content

RFC: Obsidian-Inspired Plugin System Enhancement #74

@positonic

Description

@positonic

Summary

This RFC proposes enhancing Exponential's plugin system to support lifecycle hooks, a rich App Context API, an event system, and programmatic registration - similar to Obsidian's architecture.

Motivation

Our current plugin system uses static manifest-driven registration. While this works, it lacks:

  • Lifecycle management - No way for plugins to run initialization/cleanup code
  • Programmatic registration - Everything must be declared in manifests upfront
  • Event system - Plugins cannot react to app events
  • Hot enable/disable - Requires page refresh when toggling plugins

Obsidian's plugin system (1,500+ community plugins) demonstrates that lifecycle hooks and a rich API enable powerful extensibility.

Proposed Features

1. Plugin Base Class with Lifecycle Hooks

export abstract class Plugin {
  abstract onload(): void | Promise<void>;
  abstract onunload(): void | Promise<void>;
  
  protected registerNavigation(item: NavigationItem): string;
  protected registerWidget(widget: DashboardWidget): string;
  protected registerCommand(command: PluginCommand): string;
  protected on<E extends AppEventType>(event: E, callback: AppEventCallback<E>): () => void;
}

2. App Context API

Plugins receive an app object providing controlled access to:

  • Workspace context (id, slug, name)
  • Event subscription
  • tRPC API access
  • UI helpers (notify, openModal)
  • Settings persistence

3. Event System

type AppEventType =
  | 'workspace:changed'
  | 'action:created' | 'action:completed'
  | 'goal:created' | 'goal:updated'
  | 'plugin:loaded' | 'plugin:unloaded';

4. Command Registration (Keyboard Shortcuts)

this.registerCommand({
  id: 'open-okrs',
  name: 'Open OKRs Dashboard',
  hotkey: 'mod+shift+o',
  callback: () => { /* navigate */ }
});

Example: Migrated OKR Plugin

export class OkrPlugin extends Plugin {
  async onload() {
    // Register navigation programmatically
    this.registerNavigation({
      label: 'OKRs', icon: 'IconTargetArrow',
      href: '/w/:workspaceSlug/okrs', section: 'workspace', order: 5
    });

    // Register widget with actual component
    this.registerWidget({
      title: 'OKR Progress',
      component: OkrProgressWidget,
      order: 10, gridSpan: 'half'
    });

    // Subscribe to events
    this.on('goal:created', (payload) => {
      this.app.notify({ title: 'Goal Created', message: 'Consider adding Key Results!', type: 'info' });
    });
  }

  async onunload() {
    // Cleanup handled automatically
  }
}

Backwards Compatibility

  • Manifest-only plugins continue to work unchanged
  • Existing navigation/widgets from manifests are registered if no onload() overrides
  • tRPC routers remain statically imported
  • Database schema unchanged
  • Plugins can be migrated incrementally

Files to Create/Modify

New Files

  • src/plugins/events.ts - Event bus and types
  • src/plugins/app.ts - PluginApp interface
  • src/providers/PluginProvider.tsx - React context for lifecycle
  • src/plugins/okr/OkrPlugin.ts - Reference implementation

Modified Files

  • src/plugins/types.ts - Add Plugin class, PluginCommand
  • src/plugins/registry.ts - Add lifecycle methods
  • src/plugins/loader.ts - Support PluginClass
  • src/app/_components/PluginWidgets.tsx - Use PluginProvider
  • src/hooks/usePluginNavigation.ts - Use PluginProvider
  • src/app/(sidemenu)/layout.tsx - Add PluginProvider

Questions for Discussion

  1. Event granularity - What events should be exposed to plugins?
  2. Command palette - Should we add a command palette UI (like Obsidian's Cmd+P)?
  3. Plugin settings UI - Should plugins be able to define their own settings schema?
  4. Security - Any concerns with giving plugins access to the tRPC API?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions