Skip to content

Plugin System Overview

Aether uses an extension-based architecture where plugins register functionality at predefined extension points throughout the platform. This allows plugins to extend the system without modifying core code.

Extension points are specific locations in the UI where plugins can add functionality:

'work-unit.detail.tabs' // Add tabs to work unit details
'work-unit.header.actions' // Add action buttons to work unit headers
'customer.detail.tabs' // Add tabs to customer details
'dashboard.widgets' // Add dashboard widgets
'sidebar.widgets' // Add sidebar components
// ... many more

Every plugin has a manifest that declares:

  • Plugin metadata (name, version, author)
  • What extensions it provides
  • Where those extensions appear
  • What components to render
  • Lifecycle hooks
export const myPlugin: PluginManifest = {
slug: 'my-plugin',
name: 'My Plugin',
version: '1.0.0',
description: 'Plugin description',
extensions: [
{
point: 'work-unit.detail.tabs',
plugin: 'my-plugin',
priority: 50,
extension: {
type: ExtensionType.TAB,
id: 'my-tab',
label: 'My Tab',
component: MyTabComponent,
}
}
],
};

Aether supports five extension types:

  1. TAB - Add custom tabs to detail views
  2. ACTION - Add buttons and actions
  3. WIDGET - Add dashboard/sidebar components
  4. COLUMN - Add custom table columns
  5. INDICATOR - Add badges and status markers

When the application starts:

apps/web/src/lib/plugins/manifest-registry.generated.ts
export const ALL_PLUGIN_MANIFESTS: PluginManifest[] = [
timerPlugin,
tasksPlugin,
discussionsPlugin,
// ... all discovered plugins
];

The platform loads all manifests and builds an extension registry:

const registry = new ExtensionRegistry();
ALL_PLUGIN_MANIFESTS.forEach(manifest => {
manifest.extensions.forEach(ext => {
registry.register(ext.point, ext);
});
});

When the UI needs to render an extension point:

function WorkUnitDetailTabs({ workUnit }) {
// Get all extensions for this point
const extensions = registry.getExtensions('work-unit.detail.tabs', {
businessType: workUnit.businessType,
// ... context
});
return (
<Tabs>
{extensions.map(ext => (
<Tab key={ext.id} label={ext.label} icon={ext.icon}>
<ext.component context={{ workUnit, ... }} />
</Tab>
))}
</Tabs>
);
}

Plugins follow strict isolation principles:

  • Import from @aether/plugins-core (plugin framework)
  • Import from @aether/ui (shared UI components)
  • Import from @aether/database (Prisma client for DB access)
  • Import from own plugin package
  • Use tRPC for server communication
  • Import from apps/web/src (web app internals)
  • Import from other plugin packages
  • Direct database access without Prisma
  • Modify global state
  • Import React/Next.js directly (use @aether/ui exports)

Example:

// ✅ CORRECT
import { Card, Text, Stack } from '@aether/ui';
import { IconClock } from '@aether/ui/icons';
import { trpc } from '@/lib/trpc/client';
// ❌ WRONG
import { Card } from '@mantine/core'; // Use @aether/ui instead
import { SomeComponent } from 'apps/web/src/components'; // Never import from web app
import { otherPlugin } from '@aether/plugin-other'; // No cross-plugin imports
  1. Plugin package added to packages/plugins/
  2. npm install installs dependencies
  3. npm run generate:manifests discovers plugin
  4. Manifest added to registry
  1. User enables plugin in Settings > Plugins
  2. onEnable lifecycle hook called
  3. Plugin extensions registered
  4. UI updates with new extensions
  1. User navigates to a view with plugin extensions
  2. Platform checks if plugin is enabled
  3. Platform evaluates match conditions
  4. Extension component rendered with context
  1. User disables plugin in Settings > Plugins
  2. onDisable lifecycle hook called
  3. Extensions unregistered
  4. UI updates to remove extensions