How to Add a Custom Plugin to the Kaneo Project: A Complete Developer's Guide
To add a custom plugin to Kaneo, implement the IntegrationPlugin interface in apps/api/src/plugins/, define event handlers for task lifecycle events, and register your plugin via registerPlugin() in apps/api/src/plugins/index.ts.
Kaneo is an open-source project management platform with an extensible backend architecture. The plugin system allows developers to integrate external services by reacting to task events through a type-safe TypeScript interface defined in the apps/api package.
Understanding the Kaneo Plugin Architecture
The Kaneo backend uses an event-driven plugin system centered around the IntegrationPlugin interface. According to the source code in apps/api/src/plugins/types.ts, each plugin must export an object containing a unique type identifier, display name, optional configuration schema, and event handler functions.
The system uses a central registry pattern (apps/api/src/plugins/registry.ts) to maintain a map of all available plugins. When tasks are created, updated, or deleted, the system dispatches events to registered plugins that implement the corresponding handlers.
Step-by-Step Guide to Creating a Custom Plugin
Step 1: Scaffold Your Plugin Directory
Create a new directory under apps/api/src/plugins/ using your plugin name as the folder name. Follow the structure used by existing integrations like github or slack:
apps/api/src/plugins/my-integration/
├── index.ts # Main plugin export
├── config.ts # Configuration schema (optional)
└── events/ # Event handler implementations
└── task-created.ts
Step 2: Define Configuration Validation
If your integration requires API keys or webhook URLs, define a validation schema using Valibot or Zod. Create apps/api/src/plugins/my-integration/config.ts:
import { v } from "valibot";
export const myPluginConfigSchema = v.object({
webhookUrl: v.string([v.url()]),
apiKey: v.string(),
});
export type MyPluginConfig = v.InferOutput<typeof myPluginConfigSchema>;
Step 3: Create Event Handlers
Implement handler functions that react to Kaneo events. Each handler receives the event payload and an IntegrationContext object containing the database connection and plugin configuration. Create apps/api/src/plugins/my-integration/events/task-created.ts:
import type { IntegrationContext } from "../../types";
import type { TaskCreatedEvent } from "@kaneo/api/events";
import type { MyPluginConfig } from "../config";
export async function onTaskCreated(
event: TaskCreatedEvent,
ctx: IntegrationContext,
) {
const cfg = ctx.config as MyPluginConfig;
await fetch(cfg.webhookUrl, {
method: "POST",
headers: {
"Authorization": `Bearer ${cfg.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(event),
});
}
Step 4: Implement the IntegrationPlugin Interface
Export the main plugin object in apps/api/src/plugins/my-integration/index.ts:
import { IntegrationPlugin } from "../types";
import { myPluginConfigSchema } from "./config";
import { onTaskCreated } from "./events/task-created";
export const myIntegration: IntegrationPlugin = {
type: "my-integration",
name: "My Custom Integration",
configSchema: myPluginConfigSchema,
onTaskCreated,
// Add other handlers: onTaskStatusChanged, onTaskDeleted, etc.
};
Step 5: Register Your Plugin
Import and register your plugin in apps/api/src/plugins/index.ts:
import { myIntegration } from "./my-integration";
// Existing registrations...
registerPlugin(myIntegration);
Complete Example: Building a Webhook Plugin
Below is a complete working example for a webhook notification plugin that posts task status changes to an external endpoint.
Configuration schema (apps/api/src/plugins/example-webhook/config.ts):
import { v } from "valibot";
export const exampleWebhookConfigSchema = v.object({
endpoint: v.string([v.url()]),
secret: v.string(),
});
export type ExampleWebhookConfig = v.InferOutput<typeof exampleWebhookConfigSchema>;
Event handler (apps/api/src/plugins/example-webhook/events/task-status-changed.ts):
import type { IntegrationContext } from "../../types";
import type { TaskStatusChangedEvent } from "@kaneo/api/events";
import type { ExampleWebhookConfig } from "../config";
export async function onTaskStatusChanged(
event: TaskStatusChangedEvent,
ctx: IntegrationContext,
) {
const cfg = ctx.config as ExampleWebhookConfig;
await fetch(cfg.endpoint, {
method: "POST",
headers: {
"X-Webhook-Secret": cfg.secret,
"Content-Type": "application/json",
},
body: JSON.stringify({
taskId: event.taskId,
newStatus: event.status,
timestamp: event.timestamp,
}),
});
}
Plugin registration (apps/api/src/plugins/example-webhook/index.ts):
import { IntegrationPlugin } from "../types";
import { exampleWebhookConfigSchema } from "./config";
import { onTaskStatusChanged } from "./events/task-status-changed";
export const exampleWebhook: IntegrationPlugin = {
type: "example-webhook",
name: "Example Webhook",
configSchema: exampleWebhookConfigSchema,
onTaskStatusChanged,
};
Handling Database Schema Changes
If your plugin requires persistent storage for its own data, modify apps/api/src/database/schema.ts to add your tables. After updating the schema, generate a migration using:
pnpm --filter @kaneo/api db:generate
Migrations apply automatically when the API server starts.
Testing Your Custom Plugin
Place unit tests under tests/api/my-integration/ following the patterns in existing plugin tests. For integration testing, add test cases in tests/api-integration/ that create tasks and verify your plugin's side effects using mock HTTP servers or test database assertions.
Run the full validation suite before submitting:
pnpm lint
pnpm typecheck
pnpm build
Summary
- Create a new directory under
apps/api/src/plugins/with your plugin name. - Define an
IntegrationPluginobject implementing the interface fromapps/api/src/plugins/types.ts. - Implement event handlers that receive event payloads and
IntegrationContextcontaining database and config access. - Register your plugin by importing it into
apps/api/src/plugins/index.tsand callingregisterPlugin(). - Validate your code using Valibot or Zod schemas for type-safe configuration handling.
- Migrate any new database tables via
apps/api/src/database/schema.tsif your plugin requires custom storage.
Frequently Asked Questions
What events can my plugin listen to?
Your plugin can implement handlers for task lifecycle events including onTaskCreated, onTaskStatusChanged, onTaskDeleted, and onTaskCommentAdded. Each handler receives a strongly typed event payload and an IntegrationContext object. Check apps/api/src/plugins/types.ts for the complete list of available event interfaces.
Can I write plugins in JavaScript instead of TypeScript?
While the Kaneo codebase is written in TypeScript, you can write plugins in JavaScript. However, you will lose type safety and IntelliSense for the IntegrationContext and event payloads. The build system will transpile JavaScript files, but maintaining TypeScript definitions ensures compatibility with the registry system.
How do I access the database within my plugin handlers?
The IntegrationContext object passed to every event handler contains a database connection property. Use this context to query or modify data using the ORM patterns established in the Kaneo codebase. The context also provides access to the validated plugin configuration specific to your integration instance.
Where should I store plugin configuration?
Store configuration schemas in a config.ts file within your plugin directory. When administrators configure your plugin through the Kaneo UI, the system validates inputs against your schema before persisting them to the database. Your event handlers receive the validated configuration through the IntegrationContext object.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →