# How the Kaneo Plugin Architecture Works: A Technical Deep Dive

> Explore Kaneo's plugin architecture. Learn how its registry system discovers, stores, and dispatches integration plugins using type-safe event handlers and webhooks.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: deep-dive
- Published: 2026-08-30

---

**Kaneo's plugin architecture implements a registry-based system that discovers, stores, and dispatches integration plugins through type-safe event handlers and optional webhook endpoints.**

The [Kaneo](https://github.com/usekaneo/kaneo) project management platform uses a modular plugin system to keep the core API thin while supporting deep integrations with external services like GitHub, Slack, and Gitea. Understanding how the Kaneo plugin architecture works reveals a design that isolates third-party logic through event broadcasting, allowing developers to add new integrations without modifying core application code.

## Defining the Plugin Interface

Every integration in Kaneo implements the `IntegrationPlugin` type defined in [[`apps/api/src/plugins/types.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts). This interface establishes the contract between the core API and third-party extensions.

The `IntegrationPlugin` type requires:

- **`type`** and **`name`** – A unique machine identifier and human-readable label
- **Event handlers** – Optional async functions for task lifecycle events (`onTaskCreated`, `onTaskStatusChanged`, etc.)
- **`handleWebhook`** – An optional method for processing inbound webhooks from external services
- **`getTaskMetadata`** – An optional provider for enriching tasks with external data
- **`validateConfig`** – A required function ensuring user-supplied configuration is well-formed

```typescript
// apps/api/src/plugins/types.ts
export interface IntegrationPlugin {
  type: string;
  name: string;
  onTaskCreated?: (event: TaskEvent, ctx: PluginContext) => Promise<void>;
  onTaskStatusChanged?: (event: TaskStatusEvent, ctx: PluginContext) => Promise<void>;
  handleWebhook?: (request: WebhookRequest, ctx: PluginContext) => Promise<WebhookResult>;
  getTaskMetadata?: (taskId: string, ctx: PluginContext) => Promise<Metadata>;
  validateConfig: (config: unknown) => Promise<ValidationResult>;
}

```

## Registering Plugins with the Registry

Plugins are stored in a global `Map` managed by the registry in [[`apps/api/src/plugins/registry.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/registry.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/registry.ts). The `registerPlugin` function handles installation during server initialization.

When `registerPlugin` is called, the system:

1. Validates that the plugin's `type` is unique to prevent collisions
2. Stores the plugin object in the registry's internal Map
3. Makes the plugin available for event dispatch and webhook routing

```typescript
// Example: Registering a custom notifier plugin
import { registerPlugin } from "@kaneo/plugins/registry";
import { IntegrationPlugin } from "@kaneo/plugins/types";

const notifierPlugin: IntegrationPlugin = {
  type: "notifier",
  name: "Simple Notifier",
  async onTaskCreated(event, ctx) {
    await sendNotification(`New task ${event.title}`, ctx);
  },
  async validateConfig(config) {
    const ok = typeof (config as any).url === "string";
    return { valid: ok, errors: ok ? [] : ["url must be a string"] };
  },
};

registerPlugin(notifierPlugin);

```

## Broadcasting Task Events

The registry implements an event subscription pattern through `initializeEventSubscriptions`. When domain events like `task.created` or `task.status_changed` occur, the system invokes corresponding `broadcast*` functions (e.g., `broadcastTaskCreated`).

The broadcasting flow follows three steps:

- **Query active integrations** – `getActiveIntegrations` retrieves enabled plugins for the affected project
- **Lookup plugin handlers** – `getPlugin(integration.type)` fetches the registered plugin
- **Execute with isolation** – `createContext` builds a **plugin context** (containing integration ID, project ID, and parsed config) and invokes the handler. Errors are caught and logged to prevent a failing plugin from breaking the entire system.

```typescript
// How the registry dispatches a task-created event
import { broadcastTaskCreated } from "@kaneo/plugins/registry";

await broadcastTaskCreated({
  taskId: "t123",
  projectId: "p456",
  userId: "u789",
  title: "Add login flow",
  description: null,
  priority: "high",
  status: "open",
  number: 1,
});

```

## Handling Inbound Webhooks

Plugins that react to external services expose a `handleWebhook` function. The API routes incoming requests to specific plugin handlers based on the integration type. For example, GitHub webhooks are processed in [[`apps/api/src/plugins/github/webhook-handler.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/github/webhook-handler.ts)](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/github/webhook-handler.ts), which parses the payload and translates it into Kaneo domain events.

This pattern applies consistently across integrations:

- **GitHub** – Processes push events and issue updates
- **Slack** – Handles interactive components and slash commands
- **Gitea** – Mirrors GitHub's webhook structure for self-hosted Git

The registry routes the request to the correct plugin's `handleWebhook` method, passing the standardized `PluginContext` for authentication and configuration access.

## Configuration Validation and Metadata

Before a user can enable an integration, the `validateConfig` function ensures the provided settings are correct. This runs during integration creation and updates in the API layer, rejecting malformed configurations before they reach the plugin storage.

Additionally, the optional `getTaskMetadata` method allows plugins to inject external context into tasks. For instance, a GitHub plugin might return pull request status, while a time-tracking integration could append logged hours.

```typescript
// Example validation and metadata implementation
async validateConfig(config) {
  const { apiKey, domain } = config as { apiKey: string; domain: string };
  const errors: string[] = [];
  
  if (!apiKey || apiKey.length < 20) errors.push("Invalid API key");
  if (!domain?.includes(".")) errors.push("Valid domain required");
  
  return { valid: errors.length === 0, errors };
}

async getTaskMetadata(taskId, ctx) {
  const externalData = await fetchExternalTask(taskId, ctx.config);
  return { externalUrl: externalData.html_url, state: externalData.state };
}

```

## Summary

- **Type-safe contracts** – The `IntegrationPlugin` interface in [`types.ts`](https://github.com/usekaneo/kaneo/blob/main/types.ts) defines clear boundaries for event handlers, webhooks, and configuration
- **Registry pattern** – A global Map in [`registry.ts`](https://github.com/usekaneo/kaneo/blob/main/registry.ts) stores plugins uniquely by type and handles event dispatch with error isolation
- **Event-driven architecture** – Task lifecycle events trigger `broadcast*` functions that route to active project integrations only
- **Webhook endpoints** – Plugins expose `handleWebhook` methods to receive external service notifications without core API changes
- **Defensive validation** – `validateConfig` runs at the API layer to ensure configuration integrity before persistence

## Frequently Asked Questions

### How do I add a new third-party integration to Kaneo?

Create a new file in `apps/api/src/plugins/` that exports an object satisfying the `IntegrationPlugin` interface. Implement `validateConfig` for setup validation, add event handlers like `onTaskCreated` for task automation, and optionally include `handleWebhook` if the service sends inbound notifications. Import your plugin in the server initialization file and call `registerPlugin`.

### What happens if a plugin throws an error during event handling?

The registry wraps plugin handler invocations in try-catch blocks within the broadcasting functions (e.g., `broadcastTaskCreated`). Errors are logged to the console but do not interrupt the processing of other active plugins or the core API response, ensuring fault isolation between integrations.

### Can a plugin modify the task data before it is saved?

According to the current architecture in [`types.ts`](https://github.com/usekaneo/kaneo/blob/main/types.ts), event handlers receive event payloads and contexts but do not return modified data. The system currently supports one-way event notifications (`onTaskCreated`, `onTaskStatusChanged`) rather than middleware-style request modification. For enriching task display data, use the `getTaskMetadata` method instead.