How the Kaneo Plugin Architecture Works: A Technical Deep Dive

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 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). 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
// 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). 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
// 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.
// 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), 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.

// 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 defines clear boundaries for event handlers, webhooks, and configuration
  • Registry pattern – A global Map in 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →