Skip to content

Plugin Development

A plugin extends the panel itself: extra API routes, admin and server tabs, scheduled work, WebSocket messages, and plugin-owned storage. It is not the same thing as a game-server mod. Server owners install those from Mods & Plugins; this guide is for people who write code that runs inside the panel.

If you only need to call Catalyst from outside, use the REST API instead.

Capability Where it shows up
HTTP routes /api/plugins/<name>/…, same session and API key auth as the rest of the panel
Admin and server tabs Sidebar and server detail pages
Standalone pages A path such as /my-plugin
UI slots Dashboard, sidebar, and profile
Scheduled work Re-registered every time the plugin is enabled
Events Host lifecycle (server:started) and your own events
Storage Key-value plus document collections, scoped to the plugin
Sign-in “Continue with …” buttons on the login page
Role permissions Your own permission ids, listed in the role editor and usable to gate your UI
License validation Vendor-owned license checks and encrypted payloads
Plugin-to-plugin calls Named functions another plugin can invoke

Plugins run in the panel process. A thrown error in your code can take the backend down with it. There is no worker isolation to rely on: isolation is not implemented, so a manifest that declares "runtime": "isolated" is refused at load time instead of running in-process.

Every plugin is one directory whose name equals the manifest name:

my-plugin/
plugin.json required manifest
backend/index.js optional backend entry (ESM)
frontend/index.ts optional UI entry
frontend/frontend.mjs optional self-contained UI bundle
README.md optional
assets/ optional static files

That layout is what the official catalyst-plugins repository uses, and it is what the panel installer accepts. A package may contain only plugin.json, README.md, LICENSE, backend/, frontend/, and assets/. Anything else is rejected.

File Job
plugin.json Identity, permissions, config fields, events. Validated before the plugin loads. See Manifest.
backend entry Default-exports { onLoad, onEnable, onDisable, onUnload }. See Backend.
frontend entry Default-exports a frontend plugin definition. See Frontend.

Marketplace package. Build a .catpkg.zip, publish it from an index, and admins install it from Admin → Extensions → Marketplace. The package stays inert until an admin accepts the safety disclaimer and enables it. This is how third-party plugins reach a panel. See Packaging.

Repository checkout. The panel image compiles first-party plugins from the catalyst-plugins repository at build time. That path shares the panel’s React copy, so React hooks work. A marketplace install that was not compiled into the image falls back to frontend.mjs, which cannot use React hooks. If your UI calls useState or useEffect, ship it as a repository plugin or keep the UI free of hooks.

  1. Create a plugin — scaffold, install, enable.
  2. Manifest — every plugin.json field.
  3. Backend — routes, tasks, events, WebSocket, files.
  4. Frontend — tabs, pages, slots.
  5. Data & permissions — config, storage, what you may read.
  6. Sign-in plugins — OAuth and SSO.
  7. Plugin licensing — vendor-owned license validation and encrypted payloads.
  8. Packaging & marketplace — .catpkg.zip and indexes.
  9. API reference — the host REST API a plugin can call.
  • Nothing executes before an accepted safety disclaimer. Discovery reads plugin.json only; the backend module is imported and onLoad runs when an admin enables the plugin. A version or permission change forces a fresh acceptance and leaves the new code disabled until it happens.
  • Declare the smallest permission set you actually use. Admins see that list before they enable the plugin, and every host capability is gated on it.
  • Never put secrets in plugin.json or in source. Put them in config fields of type password, and document what the operator must set.
  • Register HTTP routes in onLoad. Register tasks, WebSocket handlers, and event listeners in onEnable, because disable clears them.
  • Gate your routes on the caller, not only on your own grant: config.requiredPermissions is enforced by the host. See Backend.
  • request.user.userId is the authenticated user id. Use context.getUserId(request).
  • context.getConfig(key) returns the configured value, not the schema object.