Plugin System
Overview
The Talawa API Plugin System is a robust, event-driven plugin architecture designed for extending the GraphQL API backend without modifying core code. It provides a complete lifecycle management system for server-side plugins with support for GraphQL extensions, database schema extensions, hooks, webhooks, and Docker container management.
Key Features
- Builder-First GraphQL Extensions: Pothos GraphQL builder integration for type-safe schema extensions
- Database Extensions: Dynamic table creation and management with Drizzle ORM
- Event Hooks: Pre/post event hooks for plugin lifecycle and business logic
- Webhook Support: RESTful webhook endpoints for external integrations
- Docker Integration: Automatic container lifecycle management for plugins requiring services
- Dependency Management: Automatic npm package installation for plugin dependencies
- Event-Driven Architecture: EventEmitter-based pub/sub system for plugin communication
- Database as Source of Truth: Plugin state persisted in database for reliability
- Type-Safe: Full TypeScript support with comprehensive type definitions
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Plugin Manager (Core) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Lifecycle │ │ Extensions │ │ Registry │ │
│ │ Manager │ │ Loader │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼──────────────────┐
│ │ │
┌───────▼────────┐ ┌──────▼──────┐ ┌────────▼───────── ┐
│ Extension │ │ Database │ │ Event System │
│ Registry │ │ Operations │ │ (EventEmitter) │
├────────────────┤ ├─────────────┤ ├──────────────────┤
│ • GraphQL │ │ • Plugin DB │ │ • Lifecycle │
│ (Pothos) │ │ • Tables │ │ Events │
│ • Database │ │ • Enums │ │ • Hook Events │
│ • Hooks │ │ • Relations │ │ • Custom Events │
│ • Webhooks │ │ │ │ │
└────────────────┘ └─────────────┘ └──────────────────┘
Core Files
1. index.ts
- Main Entry Point
Purpose: Central export hub for the entire API plugin system.
Responsibilities:
- Exports all public APIs, types, utilities, and managers
- Provides clean interface for consuming the plugin system
- Single source of truth for what's available to external consumers
Key Exports:
PluginManager
- Main orchestrator class- Type definitions (
IPluginManifest
,IExtensionPoints
, etc.) - Utility functions (
validatePluginManifest
,loadPluginManifest
, etc.) - Registry functions (
initializePluginSystem
,createPluginContext
)
2. types.ts
- Type Definitions
Purpose: Centralized TypeScript type definitions for the entire plugin system.
Key Type Categories:
1. Plugin Manifest Types (IPluginManifest
)
{
name: string;
pluginId: string;
version: string;
description: string;
author: string;
main: string;
extensionPoints?: IExtensionPoints;
dependencies?: Record<string, string>;
docker?: {
enabled?: boolean;
composeFile?: string;
service?: string;
// Lifecycle flags
buildOnInstall?: boolean;
upOnActivate?: boolean;
downOnDeactivate?: boolean;
removeOnUninstall?: boolean;
};
}
2. Extension Types
GraphQL Extensions (IGraphQLExtension
)
- Uses Pothos builder-first approach
builderDefinition
: Function name that defines the GraphQL field- Supports queries, mutations, and subscriptions
- Type-safe field definitions
Database Extensions (IDatabaseExtension
)
type
: "table" | "enum" | "relation"name
: Database object namefile
: Path to definition file
Hook Extensions (IHookExtension
)
type
: "pre" | "post"event
: Event name to hook intohandler
: Function name to execute
Webhook Extensions (IWebhookExtension
)
path
: Webhook endpoint pathmethod
: HTTP method (GET, POST, PUT, DELETE, PATCH)handler
: Function name for request handling
3. Runtime Types
ILoadedPlugin
- Plugin instance with loaded components:
{
id: string;
manifest: IPluginManifest;
graphqlResolvers: Record<string, unknown>;
databaseTables: Record<string, Record<string, unknown>>;
hooks: Record<string, Function>;
webhooks: Record<string, Function>;
status: PluginStatus;
}
PluginStatus
- Enum for plugin states:
ACTIVE
- Plugin is loaded and activeINACTIVE
- Plugin is loaded but not activeERROR
- Plugin encountered an errorLOADING
- Plugin is currently loading
4. Context Types
IPluginContext
- Runtime context passed to plugins:
{
db: IDatabaseClient; // Drizzle database client
graphql: unknown; // GraphQL schema/builder
pubsub: unknown; // PubSub for subscriptions
logger: ILogger; // Logger instance
}
3. registry.ts
- Plugin Registry & Initialization
Purpose: Provides initialization utilities and global plugin manager access.
Key Functions:
createPluginContext(dependencies)
Creates a plugin context object with all necessary dependencies:
const context = createPluginContext({
db: drizzleClient,
graphql: builder,
pubsub: pubsubInstance,
logger: logger
});
initializePluginSystem(context, pluginsDirectory?)
Initializes the plugin system and returns the plugin manager:
const pluginManager = await initializePluginSystem(context);
Features:
- Singleton pattern - returns same instance on subsequent calls
- Waits for
plugins:ready
event before resolving - Error handling and logging
- Async initialization
getPluginManagerInstance()
Returns the current plugin manager instance:
const manager = getPluginManagerInstance();
isPluginSystemInitialized()
Checks if plugin system is ready:
if (isPluginSystemInitialized()) {
// System is ready
}
destroyPluginSystem()
Gracefully shuts down plugin system (useful for testing):
await destroyPluginSystem();
getPluginSystemStatus()
Returns health check and status information:
const status = getPluginSystemStatus();
// { initialized: true, pluginCount: 5, ... }
4. utils.ts
- Utility Functions
Purpose: Common utility functions for plugin operations.
Key Functions:
Validation Functions
validatePluginManifest(manifest)
- Validates manifest structure and required fields
- Checks version format (semver)
- Validates pluginId format (alphanumeric + underscores, no hyphens)
- Returns type predicate for TypeScript
isValidPluginId(pluginId)
- Validates plugin ID format
- Must start with letter, can contain letters, numbers, underscores
- No hyphens (important for GraphQL field naming)
Loading Functions
loadPluginManifest(pluginPath)
- Loads and parses manifest.json
- Validates manifest structure
- Throws descriptive errors
safeRequire(filePath)
- Safely requires/imports a module
- Handles both CommonJS and ES modules
- Returns null on failure instead of throwing
File System Functions
directoryExists(path)
- Checks if directory exists
- Returns boolean
ensureDirectory(path)
- Creates directory if it doesn't exist
- Recursive creation (like
mkdir -p
)
Plugin-Specific Functions
generatePluginId(name)
- Creates valid plugin ID from name
- Lowercase, alphanumeric + underscores
normalizeImportPath(path)
- Normalizes import paths for cross-platform compatibility
Database Functions
createPluginTables(pluginId, tableDefinitions, db)
- Creates database tables for a plugin
- Uses Drizzle ORM schema definitions
dropPluginTables(pluginId, db)
- Drops all tables associated with a plugin
- Cleanup on uninstall
Helper Functions
sortExtensionPoints(extensions)
- Sorts extensions by priority/order
filterActiveExtensions(extensions)
- Filters for active extensions only
debounce(fn, delay)
- Debounces function calls
deepClone(obj)
- Deep clones objects safely
5. pluginWebhooks.ts
- Webhook Handler
Purpose: Fastify plugin that handles dynamic webhook routing for plugins.
How It Works:
- Registers wildcard routes:
/api/plugins/:pluginId/webhook/*
- Looks up webhook handler in extension registry
- Injects plugin context into request
- Executes webhook handler
- Returns response
Route Patterns:
// Primary webhook endpoint
POST /api/plugins/my-plugin/webhook
// Webhook with path
POST /api/plugins/my-plugin/webhook/callback
GET /api/plugins/my-plugin/webhook/status
Features:
- Dynamic handler lookup from extension registry
- Plugin context injection
- Error handling with proper HTTP status codes
- Support for all HTTP methods (GET, POST, PUT, DELETE, PATCH)
Integration:
// In Fastify app setup
import { pluginWebhooks } from './plugin/pluginWebhooks';
await fastify.register(pluginWebhooks);
Usage in Plugin:
// Plugin manifest
{
"extensionPoints": {
"webhooks": [
{
"path": "/callback",
"method": "POST",
"handler": "handleWebhook"
}
]
}
}
// Plugin code
export async function handleWebhook(request, reply) {
const data = request.body;
// Process webhook
reply.send({ success: true });
}
Manager Directory
1. manager/core.ts
- Core Plugin Manager
Purpose: Main orchestrator for the entire plugin system. Extends EventEmitter for event-driven architecture.
Key Responsibilities:
1. Plugin Discovery & Loading
- Discovers plugins from database (database is source of truth)
- Loads plugin manifests and modules
- Initializes plugin components
- Registers extension points
2. Plugin State Management
- Maintains
loadedPlugins
Map with plugin instances - Tracks plugin status (ACTIVE, INACTIVE, ERROR, LOADING)
- Manages plugin errors and error recovery
3. Extension Registry
- Maintains centralized registry of all extensions:
- GraphQL builder extensions
- Database tables, enums, relations
- Pre/post hooks
- Webhook handlers
4. Lifecycle Coordination
- Delegates to
PluginLifecycle
for lifecycle operations - Emits events throughout plugin lifecycle
- Coordinates with database for persistence
5. Component Coordination
- Initializes and manages:
ExtensionLoader
- Loads extension pointsPluginLifecycle
- Handles lifecycle operationsPluginRegistry
- Database operations
Key Methods:
loadPlugin(pluginId)
- Load a plugin from filesystem:
const success = await pluginManager.loadPlugin('my-plugin');
activatePlugin(pluginId)
- Activate a loaded plugin:
await pluginManager.activatePlugin('my-plugin');
// Triggers schema rebuild
deactivatePlugin(pluginId)
- Deactivate active plugin:
await pluginManager.deactivatePlugin('my-plugin');
// Triggers schema rebuild
installPlugin(pluginId)
- Install plugin (dependencies + database):
await pluginManager.installPlugin('my-plugin');
uninstallPlugin(pluginId)
- Uninstall and cleanup:
await pluginManager.uninstallPlugin('my-plugin');
getExtensionRegistry()
- Get all registered extensions:
const registry = pluginManager.getExtensionRegistry();
const graphqlExtensions = registry.graphql.builderExtensions;
getLoadedPlugins()
- Get all loaded plugins:
const plugins = pluginManager.getLoadedPlugins();
isSystemInitialized()
- Check initialization status:
if (pluginManager.isSystemInitialized()) {
// System ready
}
gracefulShutdown()
- Shutdown without deactivating plugins:
await pluginManager.gracefulShutdown();
Event System:
pluginManager.on('plugins:initializing', () => {});
pluginManager.on('plugins:initialized', (pluginIds) => {});
pluginManager.on('plugin:loading', (pluginId) => {});
pluginManager.on('plugin:loaded', (pluginId) => {});
pluginManager.on('plugin:activating', (pluginId) => {});
pluginManager.on('plugin:activated', (pluginId) => {});
pluginManager.on('plugin:deactivating', (pluginId) => {});
pluginManager.on('plugin:deactivated', (pluginId) => {});
pluginManager.on('plugin:error', (pluginId, error) => {});
2. manager/lifecycle.ts
- Lifecycle Manager
Purpose: Handles isolated plugin lifecycle processes with clear separation of concerns.
Lifecycle Operations:
1. Plugin Installation
await lifecycle.installPlugin(pluginId, pluginManager);
Actions:
- Install npm dependencies (from manifest.dependencies)
- Create plugin-defined database tables
- Call
onInstall
lifecycle hook - Build Docker container (if enabled)
- Emit
plugin:installed
event
No schema rebuild - plugin must be activated to integrate with GraphQL
2. Plugin Activation
await lifecycle.activatePlugin(pluginId, pluginManager);
Actions:
- Call
onActivate
lifecycle hook - Start Docker container (if configured)
- Update plugin status to ACTIVE
- Update database record
- Trigger schema rebuild to integrate extensions
- Emit
plugin:activated
event
3. Plugin Deactivation
await lifecycle.deactivatePlugin(pluginId, pluginManager);
Actions:
- Call
onDeactivate
lifecycle hook - Stop Docker container (if configured)
- Update plugin status to INACTIVE
- Update database record
- Trigger schema rebuild to remove extensions
- Emit
plugin:deactivated
event
4. Plugin Uninstallation
await lifecycle.uninstallPlugin(pluginId, pluginManager);
Actions:
- Call
onUninstall
lifecycle hook - Remove Docker container (if enabled)
- Drop plugin database tables
- Remove from extension registry
- Remove from loaded plugins
- Trigger schema rebuild for cleanup
- Emit
plugin:uninstalled
event
Lifecycle Hooks:
Plugins can export lifecycle functions:
// Plugin code
export async function onInstall(context: IPluginContext) {
// Setup logic
}
export async function onActivate(context: IPluginContext) {
// Activation logic
}
export async function onDeactivate(context: IPluginContext) {
// Cleanup logic
}
export async function onUninstall(context: IPluginContext) {
// Final cleanup
}
Docker Management:
Automatically manages Docker containers based on manifest:
{
"docker": {
"enabled": true,
"composeFile": "docker-compose.yml",
"service": "my-service",
"buildOnInstall": true,
"upOnActivate": true,
"downOnDeactivate": true,
"removeOnUninstall": true,
"env": {
"PLUGIN_VAR": "value"
}
}
}
Key Methods:
createPluginDatabases(pluginId, manifest)
- Create tablesremovePluginDatabases(pluginId)
- Drop tablesmanageDocker(pluginId, manifest, operation)
- Docker lifecycletriggerSchemaRebuild()
- Rebuild GraphQL schemacallOnInstallHook(pluginId)
- Call install hookcallOnActivateHook(pluginId)
- Call activate hook
3. manager/extensions.ts
- Extension Loader
Purpose: Loads and registers GraphQL, Database, Hook, and Webhook extensions from plugins.
Extension Loading Process:
1. GraphQL Extensions (Builder-First Approach)
await extensionLoader.loadExtensionPoints(pluginId, manifest, pluginModule);
Pothos Builder Integration:
// Plugin manifest
{
"extensionPoints": {
"graphql": [
{
"type": "query",
"name": "myPluginQuery",
"builderDefinition": "defineMyQuery",
"file": "graphql/queries.ts"
}
]
}
}
// Plugin code (graphql/queries.ts)
export function defineMyQuery(builder: PothosSchemaBuilder) {
builder.queryField('myPluginQuery', (t) =>
t.field({
type: 'String',
resolve: () => 'Hello from plugin!'
})
);
}
Features:
- Type-safe field definitions with Pothos
- Automatic prefixing with plugin ID
- Support for queries, mutations, subscriptions
- Error handling and validation
2. Database Extensions
// Plugin manifest
{
"extensionPoints": {
"database": [
{
"type": "table",
"name": "plugin_data",
"file": "database/tables.ts"
}
]
}
}
// Plugin code (database/tables.ts)
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
export const pluginDataTable = pgTable('plugin_data', {
id: text('id').primaryKey(),
content: text('content'),
createdAt: timestamp('created_at').defaultNow()
});
Features:
- Drizzle ORM schema definitions
- Automatic table creation/deletion
- Support for tables, enums, relations
3. Hook Extensions
// Plugin manifest
{
"extensionPoints": {
"hooks": [
{
"type": "pre",
"event": "user:create",
"handler": "beforeUserCreate",
"file": "hooks/user.ts"
}
]
}
}
// Plugin code (hooks/user.ts)
export async function beforeUserCreate(data: any) {
// Pre-processing logic
return modifiedData;
}
Features:
- Pre/post event hooks
- Async support
- Event-driven architecture
4. Webhook Extensions
// Plugin manifest
{
"extensionPoints": {
"webhooks": [
{
"path": "/callback",
"method": "POST",
"handler": "handleCallback",
"description": "Webhook callback handler"
}
]
}
}
// Plugin code
export async function handleCallback(request, reply) {
const data = request.body;
// Process webhook
reply.send({ success: true });
}
Key Methods:
loadExtensionPoints(pluginId, manifest, module)
- Load all extensionsloadBuilderGraphQLExtension(pluginId, extension, module)
- Load GraphQLloadDatabaseExtension(pluginId, extension, module)
- Load databaseloadHookExtension(pluginId, extension, module)
- Load hooksloadWebhookExtension(pluginId, extension, module)
- Load webhooks
4. manager/registry.ts
- Registry Manager
Purpose: Handles database operations for plugin state management.
Key Responsibilities:
-
Database Queries
- Fetch plugin records from database
- Query by plugin ID
- List all installed plugins
-
Database Updates
- Update plugin status (activated/deactivated)
- Update plugin metadata
- Track installation state
Key Methods:
getPluginFromDatabase(pluginId)
const plugin = await registry.getPluginFromDatabase('my-plugin');
// { id, pluginId, isActivated, isInstalled, ... }
updatePluginInDatabase(pluginId, updates)
await registry.updatePluginInDatabase('my-plugin', {
isActivated: true,
updatedAt: new Date()
});
Database Schema (from pluginsTable
):
{
id: string; // UUID
pluginId: string; // Plugin identifier
isActivated: boolean; // Activation status
isInstalled: boolean; // Installation status
backup: boolean; // Backup flag
createdAt: Date; // Creation timestamp
updatedAt: Date; // Last update timestamp
}
Integration:
- Uses Drizzle ORM for type-safe queries
- Accesses database through plugin context
- Error handling and logging
5. manager/index.ts
- Manager Exports
Purpose: Central export point for all manager components.
Exports:
export { default as PluginManager } from './core';
export { default } from './core';
export { ExtensionLoader } from './extensions';
export { PluginLifecycle } from './lifecycle';
export { PluginRegistry } from './registry';
Usage:
// Import main manager
import PluginManager from '@/plugin/manager';
// Import individual components (advanced usage)
import { ExtensionLoader, PluginLifecycle } from '@/plugin/manager';
Extension Point System
GraphQL Extensions (Builder-First)
The system uses Pothos GraphQL builder for type-safe schema extensions:
Why Builder-First?
- Type safety with TypeScript
- No code generation needed
- Better IDE support
- Easier to test
- Composable field definitions
Example:
// Manifest
{
"extensionPoints": {
"graphql": [
{
"type": "query",
"name": "getPluginData",
"builderDefinition": "defineGetPluginDataQuery"
}
]
}
}
// Plugin code
export function defineGetPluginDataQuery(builder: PothosSchemaBuilder) {
builder.queryField('myPlugin_getPluginData', (t) =>
t.field({
type: 'String',
args: {
id: t.arg.string({ required: true })
},
resolve: async (parent, args, context) => {
// Resolver logic
return data;
}
})
);
}
Automatic Prefixing: Query/mutation names are automatically prefixed with plugin ID to avoid conflicts.
Database Extensions
Drizzle ORM Integration:
// Define table
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
export const myPluginTable = pgTable('my_plugin_data', {
id: text('id').primaryKey(),
content: text('content').notNull(),
createdAt: timestamp('created_at').defaultNow()
});
// Use in plugin
export async function saveData(context: IPluginContext, data: any) {
const db = context.db as IDatabaseClient;
await db.insert(myPluginTable).values(data);
}
Features:
- Automatic table creation on install
- Automatic table deletion on uninstall
- Type-safe queries
- Migration support
Hook Extensions
Event-Driven Hooks:
// Pre-hook (can modify data)
export async function beforeUserCreate(data: any, context: IPluginContext) {
// Validation or modification
data.customField = await fetchSomeData();
return data;
}
// Post-hook (side effects only)
export async function afterUserCreate(user: any, context: IPluginContext) {
// Notifications, logging, etc.
await sendWelcomeEmail(user.email);
}
Common Events:
user:create
,user:update
,user:delete
post:create
,post:update
,post:delete
event:create
,event:update
,event:delete
- Custom plugin events
Webhook Extensions
RESTful Webhook Endpoints:
export async function handleWebhook(
request: FastifyRequest,
reply: FastifyReply
) {
const data = request.body;
const context = (request as any).pluginContext;
// Process webhook
const result = await processData(data, context);
reply.send({ success: true, result });
}
Webhook URL Pattern: /api/plugins/{pluginId}/webhook/{path}
Plugin Development Workflow
1. Plugin Structure
my-plugin/
├── manifest.json # Plugin metadata and extensions
├── index.ts # Main entry point
├── graphql/
│ ├── queries.ts # GraphQL query definitions
│ └── mutations.ts # GraphQL mutation definitions
├── database/
│ └── tables.ts # Database schema definitions
├── hooks/
│ └── user.ts # Event hooks
├── webhooks/
│ └── handler.ts # Webhook handlers
├── docker-compose.yml # Optional Docker services
└── package.json # Plugin dependencies
2. Manifest Example
{
"name": "My Awesome Plugin",
"pluginId": "myAwesomePlugin",
"version": "1.0.0",
"description": "Adds cool features to Talawa API",
"author": "Your Name",
"main": "index.ts",
"dependencies": {
"axios": "^1.0.0",
"lodash": "^4.17.21"
},
"extensionPoints": {
"graphql": [
{
"type": "query",
"name": "getAwesomeData",
"builderDefinition": "defineGetAwesomeData",
"file": "graphql/queries.ts"
}
],
"database": [
{
"type": "table",
"name": "awesome_data",
"file": "database/tables.ts"
}
],
"hooks": [
{
"type": "post",
"event": "user:create",
"handler": "afterUserCreate",
"file": "hooks/user.ts"
}
],
"webhooks": [
{
"path": "/callback",
"method": "POST",
"handler": "handleCallback"
}
]
},
"docker": {
"enabled": true,
"composeFile": "docker-compose.yml",
"service": "my-service",
"buildOnInstall": true,
"upOnActivate": true,
"downOnDeactivate": true
}
}
3. Plugin Code Example
// index.ts
import type { IPluginContext } from '@/plugin/types';
export { defineGetAwesomeData } from './graphql/queries';
export { awesomeDataTable } from './database/tables';
export { afterUserCreate } from './hooks/user';
export { handleCallback } from './webhooks/handler';
// Lifecycle hooks
export async function onInstall(context: IPluginContext) {
context.logger.info('Plugin installed!');
}
export async function onActivate(context: IPluginContext) {
context.logger.info('Plugin activated!');
}
4. Installation & Usage
// Initialize plugin system
import { initializePluginSystem, createPluginContext } from '@/plugin';
const context = createPluginContext({
db: drizzleClient,
graphql: builder,
pubsub: pubsubInstance,
logger: logger
});
const pluginManager = await initializePluginSystem(context);
// Install plugin
await pluginManager.installPlugin('myAwesomePlugin');
// Activate plugin
await pluginManager.activatePlugin('myAwesomePlugin');
// Plugin extensions are now active and integrated!
Best Practices
For Plugin Developers
- Use Builder-First GraphQL - Always use Pothos builder definitions
- Validate Plugin IDs - No hyphens, alphanumeric + underscores only
- Handle Errors Gracefully - Implement proper error handling
- Document Extensions - Add descriptions to all extensions
- Test Lifecycle Hooks - Ensure install/uninstall work correctly
- Version Properly - Use semantic versioning
- Declare Dependencies - List all npm packages in manifest
For System Integrators
- Initialize Early - Initialize plugin system during app startup
- Wait for Ready - Wait for
plugins:ready
event - Handle Events - Subscribe to lifecycle events for monitoring
- Use Singleton - Access via
getPluginManagerInstance()
- Graceful Shutdown - Call
gracefulShutdown()
on app close
For Administrators
- Database First - Plugin state is in database, filesystem is secondary
- Test in Development - Validate plugins before production
- Monitor Docker - Check container health if using Docker features
- Backup Database - Backup before installing/uninstalling plugins
- Check Dependencies - Review npm packages for security
Event System
The plugin manager extends EventEmitter for comprehensive event-driven architecture:
Lifecycle Events
pluginManager.on('plugins:initializing', () => {
console.log('Plugin system starting...');
});
pluginManager.on('plugins:initialized', (pluginIds: string[]) => {
console.log('Loaded plugins:', pluginIds);
});
pluginManager.on('plugin:loading', (pluginId: string) => {
console.log(`Loading ${pluginId}...`);
});
pluginManager.on('plugin:loaded', (pluginId: string) => {
console.log(`${pluginId} loaded successfully`);
});
pluginManager.on('plugin:activating', (pluginId: string) => {
console.log(`Activating ${pluginId}...`);
});
pluginManager.on('plugin:activated', (pluginId: string) => {
console.log(`${pluginId} is now active`);
});
pluginManager.on('plugin:error', (pluginId: string, error: Error) => {
console.error(`Plugin ${pluginId} error:`, error);
});
Custom Plugin Events
// Plugin can emit custom events
export function defineCustomMutation(builder: PothosSchemaBuilder) {
builder.mutationField('doSomething', (t) =>
t.field({
type: 'Boolean',
resolve: async (parent, args, context) => {
// Emit custom event
context.pluginManager.emit('myPlugin:customEvent', { data: args });
return true;
}
})
);
}
// Other plugins or app can listen
pluginManager.on('myPlugin:customEvent', (data) => {
console.log('Custom event received:', data);
});
Testing
Unit Tests
import { validatePluginManifest, generatePluginId } from '@/plugin/utils';
describe('Plugin Utils', () => {
test('validates manifest', () => {
const manifest = {
name: 'Test Plugin',
pluginId: 'testPlugin',
version: '1.0.0',
description: 'Test',
author: 'Tester',
main: 'index.ts'
};
expect(validatePluginManifest(manifest)).toBe(true);
});
test('generates plugin ID', () => {
expect(generatePluginId('My Plugin!')).toBe('my_plugin');
});
});
Integration Tests
describe('Plugin Manager', () => {
let pluginManager: PluginManager;
beforeAll(async () => {
const context = createPluginContext({ /* ... */ });
pluginManager = await initializePluginSystem(context);
});
test('loads plugin', async () => {
const success = await pluginManager.loadPlugin('testPlugin');
expect(success).toBe(true);
});
test('activates plugin', async () => {
await pluginManager.activatePlugin('testPlugin');
const plugin = pluginManager.getLoadedPlugin('testPlugin');
expect(plugin?.status).toBe(PluginStatus.ACTIVE);
});
afterAll(async () => {
await destroyPluginSystem();
});
});
Troubleshooting
Plugin Not Loading
Symptoms: Plugin doesn't appear in loaded plugins list
Solutions:
- Check plugin exists in database:
SELECT * FROM plugins WHERE plugin_id = 'myPlugin'
- Verify plugin directory exists:
src/plugin/available/myPlugin/
- Validate manifest:
validatePluginManifest(manifest)
- Check console for error messages
- Verify
isInstalled
is true in database
GraphQL Extension Not Working
Symptoms: Query/mutation not available in schema
Solutions:
- Ensure plugin is activated (not just loaded)
- Check builder function is exported correctly
- Verify
builderDefinition
name matches exported function - Look for schema rebuild logs
- Test builder function manually
Database Tables Not Created
Symptoms: Tables don't exist after installation
Solutions:
- Check database extension is defined in manifest
- Verify table definition is exported correctly
- Check for errors in
onInstall
lifecycle - Manually verify database schema
- Check Drizzle ORM configuration
Webhook Not Responding
Symptoms: Webhook endpoint returns 404
Solutions:
- Verify webhook is registered:
pluginManager.getExtensionRegistry().webhooks
- Check webhook path format (should start with
/
) - Ensure plugin is activated
- Verify Fastify plugin is registered
- Check handler function signature
Docker Container Issues
Symptoms: Container not starting or stopping
Solutions:
- Verify Docker is running
- Check
docker-compose.yml
exists - Validate service name in manifest
- Check Docker logs:
docker-compose logs
- Manually test:
docker-compose up
Future Enhancements
- Plugin Marketplace: Central repository for discovering plugins
- Version Management: Support multiple plugin versions
- Plugin Dependencies: Declare dependencies on other plugins
- GraphQL Federation: Federated schema support
- Performance Monitoring: Track plugin performance impact
- Sandboxing: Isolate plugin execution contexts
- Hot Reloading: Update plugins without restart
- Permission System: Fine-grained plugin permissions
Contributing
When extending the plugin system:
- Follow existing architectural patterns
- Add comprehensive tests for new features
- Update type definitions
- Document new functionality
- Maintain backward compatibility
- Consider performance implications
License
See main project LICENSE file.