Skip to content

Create a Plugin

  • Node.js 20 or newer.
  • A running Catalyst panel you can sign in to as an administrator.
  • A checkout of the Catalyst repository. The SDK and its CLI live in that repo (packages/plugin-sdk). They are not published to the npm registry, so npx @catalyst/plugin-sdk-cli and npm install @catalyst/plugin-sdk do not work against the public registry.

The SDK is a workspace project; the CLI is not, so build it by path. From the Catalyst checkout root:

Terminal window
pnpm --filter @catalyst/plugin-sdk run build
pnpm --dir packages/plugin-sdk/cli run build
node packages/plugin-sdk/cli/dist/index.js create my-plugin --template fullstack
cd my-plugin

There is no catalyst-plugin on PATH: nothing in the workspace depends on @catalyst/plugin-sdk-cli, so pnpm never links its binary, and pnpm --filter @catalyst/plugin-sdk-cli … silently matches no project. Run the CLI through node <checkout>/packages/plugin-sdk/cli/dist/index.js, or alias that. npx catalyst-plugin does not work either, because the package is not on npm.

create options are --template/-t, --path/-p (parent directory, default .), and --typescript/-ts. The default template is backend-only; --template is one of:

Template What you get
fullstack plugin.json, a TypeScript backend, and a React frontend with an admin tab and a server tab.
backend-only plugin.json and a TypeScript backend that registers one route. No UI.
minimal plugin.json and a JavaScript entry with two lifecycle hooks. No backend or frontend key, no package.json, no tsconfig.json.

The plugin name must match ^[a-z0-9-]+$. The schema additionally caps it at 50 characters; the CLI itself only checks the pattern. The scaffold writes the name into plugin.json and into the route and tab ids, and runs git init in the new directory when git is available.

Other commands, run from the plugin directory. Substitute the absolute path to your checkout, or alias catalyst-plugin='node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js"':

Terminal window
CATALYST_REPO=/path/to/catalyst
node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" build # parse plugin.json, then npx tsc if tsconfig.json exists
node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" test # npx vitest run
node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" pack # write my-plugin-1.0.0.catpkg.zip + .sha256
node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" pack --out dist

build accepts --watch but ignores it; use npm run dev (tsc --watch) in a TypeScript scaffold instead. It only JSON-parses plugin.json and echoes name/version — it does not run schema validation, so a manifest that would be rejected at install time still “builds”. create always copies the TypeScript templates; --typescript does not switch them off. pack needs archiver resolvable (it is a devDependency of packages/plugin-sdk, hoisted to the workspace root).

The scaffold is a starting point, not the layout the official repository ships. Its backend.entry points into dist/, and a marketplace zip is not allowed to contain dist/ — see Packaging. Every plugin in catalyst-plugins instead authors backend/index.js directly and points the manifest at that file, so there is no compile step to forget. If you did compile TypeScript, copy the compiled entry to backend/index.js and repoint backend.entry before you publish.

fullstack matches the layout of catalyst-plugins:

my-plugin/
plugin.json
package.json "type": "module", build = tsc
tsconfig.json
src/index.ts backend; compiled under dist/
frontend/index.ts UI entry
frontend/components.tsx

The backend default-exports the four lifecycle hooks and registers routes in onLoad:

import { defineRoutes } from '@catalyst/plugin-sdk';
export default {
async onLoad(context) {
const routes = defineRoutes((router) => {
router.get('/hello', async () => ({ success: true, message: 'Hello' }));
});
for (const route of routes) context.registerRoute(route);
},
async onEnable(context) {
context.logger.info('enabled');
},
async onDisable() {},
async onUnload() {},
};

GET /hello is served at GET /api/plugins/my-plugin/hello.

The frontend default-exports a plugin definition. Official plugins import createFrontendPlugin from @catalyst/plugin-sdk/frontend:

import { createFrontendPlugin } from '@catalyst/plugin-sdk/frontend';
import { AdminTab, ServerTab } from './components';
export default createFrontendPlugin({
manifest: {
name: 'my-plugin',
version: '1.0.0',
displayName: 'My Plugin',
description: 'A Catalyst plugin: My Plugin',
author: 'Catalyst Developer',
},
tabs: [
{ id: 'my-plugin-admin', label: 'My Plugin', component: AdminTab, location: 'admin', order: 100 },
{ id: 'my-plugin-server', label: 'My Plugin', component: ServerTab, location: 'server', order: 100 },
],
});

A server tab component receives { serverId: string }.

Installing is an operator action in the panel, not a CLI step on the server. On the target panel:

  1. Open Admin → Extensions → Marketplace.
  2. Find your plugin in the list and select Install.
  3. Return to Admin → Extensions → Plugins. The plugin now shows in the list, disabled.

That is the whole install. The panel downloads the .catpkg.zip, verifies its sha256 against the index entry, extracts it, and validates plugin.json. An index entry without a sha256 cannot be installed. The code is on disk and not running until you enable it.

For the panel to see your plugin, publish the package first:

Terminal window
node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" pack

That writes my-plugin-1.0.0.catpkg.zip and a .sha256 sidecar in the plugin directory. Put both behind HTTPS and add an index entry with the digest from the sidecar — see Packaging. Add your index in the marketplace’s Marketplaces rail (or with PLUGIN_MARKETPLACE_URLS) if it is not the official one.

As a local checkout (the path the official repository uses): drop the directory into a checkout of catalyst-plugins and rebuild the panel image. The panel compiles frontend/index.ts into the frontend bundle. You still need frontend/frontend.mjs if the plugin should render on a panel that did not compile it in.

  1. On Admin → Extensions → Plugins, select Enable on the plugin row.
  2. The safety-consent dialog opens. Tick the acknowledgement and select Accept & Enable. It lists every permission the manifest declares. Until you do, the backend module is never imported and onLoad never runs.
  3. Confirm the route:
Terminal window
curl -H "Authorization: Bearer $CATALYST_API_KEY" \
https://panel.example.com/api/plugins/my-plugin/hello

A disabled plugin keeps its routes registered. They answer 503 until an admin enables it. Disabling does not unregister routes; it closes the gate and clears tasks and WebSocket handlers.

The SDK and its CLI are in the Catalyst repository under packages/plugin-sdk and are not on the public npm registry, so npm install @catalyst/plugin-sdk fails. A scaffold’s package.json declares "@catalyst/plugin-sdk": "workspace:*", which only resolves when the plugin directory is itself a workspace package of that checkout. In practice:

  • The frontend import works without installing anything. The panel’s catalyst-frontend/vite.config.ts aliases @catalyst/plugin-sdk/frontend to packages/plugin-sdk/src/frontend/index.ts, and the catalyst-plugins repo aliases it to its vendored sdk-frontend/ copy so bundles build without the panel checkout.
  • The backend entry cannot import the SDK package at runtime. Official plugins do not try: backend/index.js calls ctx.registerRoute directly and imports only node:* builtins and its own sibling files. The panel stages the plugin directory and imports the entry as plain ESM — there is no bare-import resolution for the SDK. If you want the SDK helpers in a backend, bundle them into your entry at build time (the same rule as frontend.mjs).
Import Use it for
@catalyst/plugin-sdk defineRoutes, defineConfig, definePermissions, defineUserPermissions, createTypedCollection, createTypedContext
@catalyst/plugin-sdk/frontend createFrontendPlugin, createPluginApi, PluginErrorBoundary
@catalyst/plugin-sdk/testing createMockContext, createTestPlugin, createMockLicense
@catalyst/plugin-sdk/config defineConfig, configField, createConfigSchema
@catalyst/plugin-sdk/routes defineRoutes, PluginRouteBuilder
@catalyst/plugin-sdk/storage createTypedCollection
@catalyst/plugin-sdk/context createTypedContext, defineTypedContext
@catalyst/plugin-sdk/licensing Backend-only vendor licensing helpers (uses node:crypto)

fastify and react are optional peer dependencies. The backend entry the panel loads must be plain ESM JavaScript — TypeScript sources are not executed.

example-plugin in the official repository is the short tour: routes, a scoped server read, a whitelisted status write, a scheduled task, server:started / server:stopped, a WebSocket ping, and key-value storage. Copy that shape, then read Backend for the contracts those calls rely on.

Selling a commercial plugin? See Plugin licensing for vendor-owned license validation and encrypted payloads.