Plugins
Plugins package reusable functionality — initializers, actions, channels, servers, and config defaults — into npm modules that any Keryx app can install and register. If you've built an initializer or a set of actions that would be useful across projects, a plugin is how you distribute it.
Using a Plugin
Install the plugin package, then add it to your config:
// config/plugins.ts
import { resqueAdminPlugin } from "@keryxjs/resque-admin";
export default {
plugins: [resqueAdminPlugin],
};That's it. The framework loads plugins during initialization — their initializers, actions, channels, and servers are discovered automatically.
The KeryxPlugin Interface
A plugin is an object that satisfies the KeryxPlugin interface:
import type { KeryxPlugin } from "keryx";
export const myPlugin: KeryxPlugin = {
name: "my-plugin",
version: "1.0.0",
// Class constructors (optional) — framework instantiates them
initializers: [MyInitializer],
actions: [MyAction, AnotherAction],
channels: [MyChannel],
servers: [MyServer],
// Config defaults (optional) — merged before user config
configDefaults: {
myPlugin: {
enabled: true,
maxRetries: 3,
},
},
// Custom generator types (optional)
generators: [
{
type: "resolver",
directory: "resolvers",
templatePath: path.join(import.meta.dir, "templates/resolver.ts.mustache"),
},
],
};All fields except name and version are optional. Provide only what your plugin needs.
What Plugins Can Provide
Initializers
Plugin initializers work exactly like framework or user initializers — they extend the Initializer class, have priority-based lifecycle hooks, and can attach namespaces to the api singleton via module augmentation:
import { Initializer } from "keryx";
declare module "keryx" {
export interface API {
cache: Awaited<ReturnType<CacheInitializer["initialize"]>>;
}
}
export class CacheInitializer extends Initializer {
constructor() {
super("cache");
this.loadPriority = 300;
}
async initialize() {
const store = new Map<string, unknown>();
return { get: (k: string) => store.get(k), set: (k: string, v: unknown) => store.set(k, v) };
}
}Users of the plugin need to import it (or the plugin package) so the module augmentation is visible to TypeScript:
import "@keryxjs/cache"; // side-effect import for type augmentationActions
Plugin actions extend Action and are registered alongside the app's own actions. They show up in HTTP routing, the CLI, MCP, and Swagger automatically:
import { Action, HTTP_METHOD, type ActionParams } from "keryx";
export class HealthCheck extends Action {
constructor() {
super({
name: "plugin:health",
description: "Extended health check from plugin",
web: { route: "/health", method: HTTP_METHOD.GET },
});
}
async run(_params: ActionParams<this>) {
return { healthy: true };
}
}Channels
Plugin channels extend Channel and are registered alongside user channels:
import { Channel } from "keryx";
export class PluginNotifications extends Channel {
constructor() {
super({ name: /^plugin:notify:.*$/, description: "Plugin notification channel" });
}
}Servers
Plugin servers extend Server and participate in the standard initialize → start → stop lifecycle.
Config Defaults
Plugin config defaults are applied using deepMergeDefaults — they only fill in values that aren't already set. User config always takes precedence. Use module augmentation to make plugin config type-safe:
declare module "keryx" {
interface KeryxConfig {
myPlugin: { enabled: boolean; maxRetries: number };
}
}Middleware
Middleware isn't registered through the plugin manifest — actions import and reference it directly. Just export your middleware from the plugin package:
// In your plugin package
export const MyPluginMiddleware: ActionMiddleware = {
runBefore: async (params, connection) => { /* ... */ },
};Users apply it to their actions:
import { MyPluginMiddleware } from "keryx-plugin-foo";
export class MyAction extends Action {
constructor() {
super({
name: "my-action",
middleware: [MyPluginMiddleware],
});
}
}Custom Generators
Plugins can register custom types for the keryx generate CLI command. Provide a Mustache template and an output directory:
{
generators: [{
type: "resolver", // `keryx generate resolver myThing`
directory: "resolvers", // output: resolvers/myThing.ts
templatePath: path.join(import.meta.dir, "templates/resolver.ts.mustache"),
testTemplatePath: path.join(import.meta.dir, "templates/resolver.test.ts.mustache"),
}]
}The template receives and as variables.
Loading Order
Understanding the loading order helps you set priorities correctly:
- User config loaded — from the app's
config/directory (includingconfig.plugins) - Plugin config defaults applied — via
deepMergeDefaults(never overwrites user-set values) - Initializer discovery — framework → plugins → user (registration order within plugins)
- Initializer execution — all initializers sorted by
loadPriority, regardless of source - Action discovery — plugin actions → user actions
- Channel discovery — plugin channels → user channels
- Server discovery — framework servers → plugin servers → user servers
Priorities control execution order across all sources. A plugin initializer with loadPriority: 50 runs before a framework initializer with loadPriority: 100.
Naming Convention
| Scope | Convention | Example |
|---|---|---|
| First-party | @keryxjs/<name> | @keryxjs/resque-admin |
| Third-party | keryx-plugin-<name> | keryx-plugin-graphql |
These are conventions, not enforced by the framework. The name field in the plugin manifest is what matters for uniqueness.
Building a Plugin Package
A minimal plugin package:
keryx-plugin-hello/
package.json
index.ts
actions/
hello.tspackage.json:
{
"name": "keryx-plugin-hello",
"version": "1.0.0",
"type": "module",
"module": "index.ts",
"peerDependencies": {
"keryx": ">=0.20.0"
}
}index.ts:
import type { KeryxPlugin } from "keryx";
import { HelloAction } from "./actions/hello";
export const helloPlugin: KeryxPlugin = {
name: "hello",
version: "1.0.0",
actions: [HelloAction],
};actions/hello.ts:
import { Action, HTTP_METHOD, type ActionParams } from "keryx";
export class HelloAction extends Action {
constructor() {
super({
name: "hello",
description: "Says hello",
web: { route: "/hello", method: HTTP_METHOD.GET },
});
}
async run(_params: ActionParams<this>) {
return { message: "Hello from plugin!" };
}
}Use keryx as a peer dependency so the app controls the framework version.