Packaging & Marketplace
Panels install plugins from a marketplace index. The official index is catalyst-plugins and is browsed by default. You can run your own index without being listed there.
Prerequisites
Section titled “Prerequisites”- A plugin directory with a valid
plugin.json— see Plugin Manifest. - A Catalyst checkout to build the SDK CLI from; the CLI is not published to npm. See Create a plugin.
- HTTPS hosting for the package. Panels refuse plaintext
httpdownloads from any host that is not a configured index host.
Package layout
Section titled “Package layout”A .catpkg.zip is a zip whose entries sit at the archive root (one wrapper
folder is flattened). The installer allows only:
| Entry | Required |
|---|---|
plugin.json |
yes |
backend/ |
no |
frontend/ |
no |
assets/ |
no |
README.md |
no |
LICENSE |
no |
Anything else is rejected, including package.json, node_modules, src/,
and dotfiles. ../, absolute paths, and links that escape the plugin
directory are rejected before anything is written.
Limits: 256 MB compressed, 512 MB extracted, 20,000 entries.
A packaged backend.entry has to live under backend/, because that is the
only allowed top-level place for backend files — the installer drops the rest.
dist/ is not an allowed top-level entry, so a scaffold’s dist/... entry
cannot ship in a package. Official plugins author backend/index.js directly
and set entry to that path. If you did compile TypeScript, copy the output to
backend/index.js and point the manifest there before packing. (The loader
itself only requires backend.entry to resolve inside the plugin directory;
the top-level allowlist is what constrains the package.)
Encrypted payloads
Section titled “Encrypted payloads”A package whose plugin.json sets licensing.encrypted: true must ship at
least one backend/*.enc payload. The installer refuses the package with
PAYLOAD_MISSING otherwise, so a mispackaged plugin fails at install time
instead of in front of an admin at enable time. plugin.json itself stays
plaintext — the panel validates it before anything runs. See
Plugin licensing.
Build the archive
Section titled “Build the archive”The CLI ships in the Catalyst repository, not on npm, and nothing links its
binary, so invoke it by path. Build it once, then pack from the plugin
directory — see Create a plugin for why
there is no catalyst-plugin command on PATH:
# once, from the Catalyst checkout rootpnpm --filter @catalyst/plugin-sdk run buildpnpm --dir packages/plugin-sdk/cli run build
# from the plugin directory ($CATALYST_REPO is your checkout)node "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" packnode "$CATALYST_REPO/packages/plugin-sdk/cli/dist/index.js" pack --out distpack needs archiver resolvable — a devDependency of packages/plugin-sdk
that pnpm install hoists to the workspace root; without an install it prints
archiver is unavailable.
pack writes <name>-<version>.catpkg.zip and <name>-<version>.catpkg.zip.sha256.
It includes plugin.json, README.md, LICENSE, and the backend/,
frontend/, and assets/ directories when they exist. It does not compile
TypeScript and it does not build frontend.mjs. Do both before packing. A
package with neither backend/ nor frontend/ installs and does nothing.
The official repository does the same thing with zip and then rewrites
index.json:
# from a plugin directory — matches scripts/build-packages.shzip -qr ../my-plugin-1.0.0.catpkg.zip plugin.json backend frontend README.md \ -x '*/node_modules/*' -x '*/.*' -x 'frontend/dist/*'sha256sum ../my-plugin-1.0.0.catpkg.zipIndex document
Section titled “Index document”Serve JSON over HTTPS. The panel accepts either a bare array or:
{ "schemaVersion": 1, "plugins": [ { "name": "my-plugin", "displayName": "My Plugin", "description": "What an admin reads in the marketplace list.", "author": "Your Name", "version": "1.0.0", "downloadUrl": "https://cdn.example.com/my-plugin-1.0.0.catpkg.zip", "sha256": "hex digest of the zip", "homepage": "https://github.com/you/my-plugin", "tags": ["notifications"] } ]}| Field | Required | Notes |
|---|---|---|
name |
yes | Must equal the packaged plugin’s plugin.json name. |
downloadUrl |
yes | Absolute URL of the .catpkg.zip. |
sha256 |
yes in practice | Lowercase hex digest of the zip. The panel lists an entry without it but refuses to install it (CHECKSUM_REQUIRED). |
version |
no | Shown in the version column; drives the update badge. |
displayName, description, author, homepage |
no | Display only. |
tags |
no | Filter chips in the marketplace; the first 12 strings are kept. |
licensing |
no | { licenseServer, buyUrl, encrypted } for the browse badge. The installed plugin.json is authoritative. |
sha256 is a hard requirement for installation: the panel verifies the
downloaded bytes against it before extracting anything, and a missing or
mismatched digest aborts the install.
downloadUrl must be https. Two exceptions are treated as trusted: a host
that is also one of your configured index hosts, and any host when the panel
sets PLUGIN_MARKETPLACE_ALLOW_LOCAL=true. Hosts that resolve to loopback,
link-local, or private addresses are refused in the same cases.
Redirects are followed up to 5 hops, and the protocol and private-address checks are re-applied to every hop, so a public URL cannot redirect the download at an internal address. Index URLs themselves are fetched as-is, including LAN hosts, because an admin configured them.
The panel caches each index for 5 minutes; browsing with a forced refresh
sends Cache-Control: no-cache. One failing source does not hide the others.
Point a panel at your index
Section titled “Point a panel at your index”An admin adds the URL in the Marketplaces rail of Admin → Extensions → Marketplace, or sets environment variables on the backend:
| Variable | Effect |
|---|---|
PLUGIN_MARKETPLACE_URLS |
Comma-separated extra index URLs, browsed together with the official index. |
PLUGIN_MARKETPLACE_DISABLE_OFFICIAL |
true skips the official index. Use on air-gapped installs. |
PLUGIN_MARKETPLACE_ALLOW_LOCAL |
true permits plaintext http downloads and loopback/private hosts other than your configured index hosts. Leave unset on a public panel. |
When two sources list the same name, the newest version wins.
Install, update, uninstall
Section titled “Install, update, uninstall”All three are clicks in Admin → Extensions — the operator steps live in Plugins. What matters when you publish:
- The panel downloads the zip, verifies
sha256, extracts, and validatesplugin.json. The code lands on disk not running. - An admin then enables it and accepts the safety disclaimer.
- A higher
versionin the index offers Update, which replaces the directory. A version or permission change forces the disclaimer again and leaves the plugin disabled until it is accepted.
POST /api/plugins/install with { "url", "sha256" } is the same flow;
sha256 is a required 64-hex digest. POST /api/plugins/:name/reload
re-reads the directory without a panel restart.
Get listed officially
Section titled “Get listed officially”Third-party indexes do not need anyone’s approval. To land in the official index, open a pull request on catalystctl/catalyst-plugins that adds one directory:
- Directory name equals
plugin.jsonname. backend.entryandfrontend.entrypoint at files that exist.- An
index.jsonentry with the samenameandversion. CI rejects a mismatch either way. - UI plugins include a
vite.config.mtsthat emitsfrontend/frontend.mjs. CI runsscripts/build-bundles.mjsbefore packing.
CI packs every directory that contains plugin.json, writes the sha256 back
into index.json, and publishes the zips on the dist branch. Download URLs
look like:
https://raw.githubusercontent.com/catalystctl/catalyst-plugins/dist/plugins/<name>-<version>.catpkg.zipVerify
Section titled “Verify”After enable:
curl -H "Authorization: Bearer $CATALYST_API_KEY" \ https://panel.example.com/api/plugins/my-pluginThe response includes the manifest, whether consent is current, and the
granted permissions. Then call one of your own routes and confirm a disabled
plugin returns 503 rather than running the handler.
Related
Section titled “Related”- Plugin Manifest — the fields the installer validates
- Create a plugin — scaffold and build the plugin first
- API reference — the panel REST API the marketplace endpoints sit beside
