# How to Create and Load Custom Plugins with PluginLoaderService in OpenWA

> Learn to create and load custom plugins in OpenWA with PluginLoaderService. Implement IPlugin, define manifest.json, and activate via API or code.

- Repository: [Yudhi Armyndharis/OpenWA](https://github.com/rmyndharis/OpenWA)
- Tags: how-to-guide
- Published: 2026-05-21

---

**To create and load custom plugins in OpenWA, implement the `IPlugin` interface in a TypeScript file, define a [`manifest.json`](https://github.com/rmyndharis/OpenWA/blob/main/manifest.json) describing the plugin metadata, place both files in a subdirectory of the configured plugins folder, and activate the plugin via the REST API or by calling `PluginLoaderService.enablePlugin()`.**

OpenWA provides a robust plugin subsystem that enables runtime extension of WhatsApp bot functionality without modifying core framework code. The `PluginLoaderService` ([`src/core/plugins/plugin-loader.service.ts`](https://github.com/rmyndharis/OpenWA/blob/main/src/core/plugins/plugin-loader.service.ts)) handles automatic discovery, validation, lifecycle management, and sandboxed execution of custom plugins. This guide covers the exact implementation details required to build, register, and enable plugins using the OpenWA plugin architecture.

## Understanding the PluginLoaderService Architecture

When the NestJS application bootstraps, `PluginLoaderService.onModuleInit()` executes a two-phase loading process:

1. **Built-in plugin registration** – Engine plugins bundled with the core are registered first (lines 47-53 in [`plugin-loader.service.ts`](https://github.com/rmyndharis/OpenWA/blob/main/plugin-loader.service.ts)).
2. **Directory scanning** – The service scans the configured plugins directory (`plugins.dir`, defaulting to `./plugins`). For each subdirectory containing a [`manifest.json`](https://github.com/rmyndharis/OpenWA/blob/main/manifest.json), it validates the manifest and stores a `PluginInstance` in an internal Map via `loadPluginsFromDirectory()` (lines 55-81).

When you enable a plugin through `enablePlugin()`, the loader dynamically requires the file declared in `manifest.main`, instantiates the default exported class (which must implement `IPlugin`), and invokes lifecycle hooks with a `PluginContext` providing access to the **HookManager**, sandboxed **PluginStorage**, namespaced logger, and configuration.

All plugin state changes persist through `PluginStorageService` in [`data/plugins/registry.json`](https://github.com/rmyndharis/OpenWA/blob/main/data/plugins/registry.json), ensuring activated plugins remain enabled across restarts.

## Creating a Custom Plugin

### Directory Structure

Place your plugin in a subdirectory of the plugins folder:

```text
<project_root>
└── plugins/
    └── my-greeting/
        ├── manifest.json
        └── index.ts

```

### Defining the Manifest

Create a [`manifest.json`](https://github.com/rmyndharis/OpenWA/blob/main/manifest.json) file that conforms to the `PluginManifest` interface defined in [`src/core/plugins/plugin.interfaces.ts`](https://github.com/rmyndharis/OpenWA/blob/main/src/core/plugins/plugin.interfaces.ts):

```json
{
  "id": "my-greeting",
  "name": "My Greeting Plugin",
  "version": "1.0.0",
  "type": "extension",
  "description": "Adds a custom greeting command to the bot.",
  "author": "Your Name",
  "main": "index.ts",
  "hooks": ["message.received"],
  "configSchema": {
    "type": "object",
    "properties": {
      "greeting": {
        "type": "string",
        "title": "Greeting Text",
        "default": "Hello, world!"
      }
    }
  }
}

```

Required fields include `id`, `name`, `version`, `type`, and `main`. The `hooks` array optionally declares events your plugin will listen to, while `configSchema` defines configurable parameters exposed through the API.

### Implementing the IPlugin Interface

Create [`index.ts`](https://github.com/rmyndharis/OpenWA/blob/main/index.ts) with a default export implementing `IPlugin` (defined in [`src/core/plugins/plugin.interfaces.ts`](https://github.com/rmyndharis/OpenWA/blob/main/src/core/plugins/plugin.interfaces.ts)):

```typescript
import { IPlugin, PluginContext } from '../../core/plugins/plugin.interfaces';

export default class MyGreetingPlugin implements IPlugin {
  async onLoad(context: PluginContext) {
    context.logger.info('MyGreetingPlugin loaded');
  }

  async onEnable(context: PluginContext) {
    context.registerHook('message.received', this.handleMessage.bind(this));
    context.logger.info('MyGreetingPlugin enabled');
  }

  async onDisable(context: PluginContext) {
    context.logger.info('MyGreetingPlugin disabled');
  }

  async onConfigChange(context: PluginContext, newConfig: Record<string, unknown>) {
    context.logger.debug('Config updated', { newConfig });
  }

  private async handleMessage(payload: any) {
    const { from, body, config } = payload;
    console.log(`Greeting ${from}: ${config?.greeting ?? 'Hi'} – ${body}`);
  }
}

```

The `onEnable` hook receives a `PluginContext` created by `createPluginContext()` (lines 61-93 in [`plugin-loader.service.ts`](https://github.com/rmyndharis/OpenWA/blob/main/plugin-loader.service.ts)), which provides the `registerHook` method for subscribing to events like `message.received`.

## Loading and Enabling Plugins

### Via REST API

The `PluginsController` ([`src/modules/plugins/plugins.controller.ts`](https://github.com/rmyndharis/OpenWA/blob/main/src/modules/plugins/plugins.controller.ts)) exposes endpoints for plugin management. After placing your plugin files in the directory and restarting OpenWA, enable the plugin:

```bash
curl -X POST http://localhost:3000/plugins/my-greeting/enable

```

Response:

```json
{ "success": true, "message": "Plugin my-greeting enabled successfully" }

```

You can also disable plugins via `POST /plugins/:id/disable` or update configuration via the plugin config endpoints.

### Programmatic Activation

Inject `PluginLoaderService` into your service or controller:

```typescript
await this.pluginLoader.enablePlugin('my-greeting');

```

Disable using:

```typescript
await this.pluginLoader.disablePlugin('my-greeting');

```

## Working with PluginContext and Storage

The `PluginContext` object provides sandboxed resources to your plugin. Access per-plugin persistent storage through `context.storage`, implemented in `PluginStorageService.createPluginStorage()` ([`src/core/plugins/plugin-storage.service.ts`](https://github.com/rmyndharis/OpenWA/blob/main/src/core/plugins/plugin-storage.service.ts)):

```typescript
// Inside any lifecycle hook or event handler
await context.storage.set('counter', 1);
const count = await context.storage.get<number>('counter');

```

The storage service creates isolated directories for each plugin under `data/plugins/`, preventing cross-plugin data access. Use `context.logger` for namespaced logging and `context.config` to access validated configuration values defined in your `configSchema`.

## Summary

- Place custom plugins in subdirectories of `./plugins` (or the path specified by `plugins.dir` configuration).
- Define a valid [`manifest.json`](https://github.com/rmyndharis/OpenWA/blob/main/manifest.json) with required fields: `id`, `name`, `version`, `type`, and `main`.
- Export a default class implementing `IPlugin` with lifecycle methods (`onLoad`, `onEnable`, `onDisable`).
- Access runtime resources through `PluginContext`, including `registerHook`, `storage`, and `logger`.
- Enable plugins via `POST /plugins/:id/enable` or `PluginLoaderService.enablePlugin()`.
- Plugin state persists automatically in [`data/plugins/registry.json`](https://github.com/rmyndharis/OpenWA/blob/main/data/plugins/registry.json) via `PluginStorageService`.

## Frequently Asked Questions

### What file format should the plugin main entry point use?

The `main` field in [`manifest.json`](https://github.com/rmyndharis/OpenWA/blob/main/manifest.json) should reference a TypeScript or JavaScript file (e.g., [`index.ts`](https://github.com/rmyndharis/OpenWA/blob/main/index.ts) or [`index.js`](https://github.com/rmyndharis/OpenWA/blob/main/index.js)) that exports a default class implementing the `IPlugin` interface. OpenWA's runtime compilation or build process handles the TypeScript files during the require operation performed by `PluginLoaderService.enablePlugin()`.

### How does OpenWA validate plugin manifests before loading?

The `PluginLoaderService.loadPluginsFromDirectory()` method validates each [`manifest.json`](https://github.com/rmyndharis/OpenWA/blob/main/manifest.json) against the `PluginManifest` interface. It checks for required fields including `id`, `name`, `version`, `type`, and `main`. If validation fails, the plugin is not registered in the internal Map and will not appear in the available plugins list.

### Can plugins be enabled or disabled without restarting the server?

Yes. While `onModuleInit()` discovers plugins at startup, you can enable or disable plugins at runtime using the REST API (`/plugins/:id/enable` and `/plugins/:id/disable`) or programmatically via `PluginLoaderService`. These calls trigger the `onEnable` and `onDisable` lifecycle hooks without requiring a server restart.

### Where does OpenWA store plugin configuration and data?

`PluginStorageService` handles persistence, storing registry state in [`data/plugins/registry.json`](https://github.com/rmyndharis/OpenWA/blob/main/data/plugins/registry.json) and providing sandboxed file-based storage through `PluginContext.storage`. Each plugin receives its own isolated storage space, accessible only through the context provided to that specific plugin instance.