Plugins
Write a plugin in plugins/<name>/, then install, activate, and manage .jfpkg packages. Manifest, permissions, and the SDK context.
8 min read
Write a plugin
If you are developing against the CE source, create a folder under `plugins/` and start there. One folder per plugin. Copy plugins/hello-world (it listens to content.published). The pnpm workspace includes plugins/*, and the server scans that directory when you run from source. Import types from @justflows/sdk only. Build to dist/index.js — the runtime does not load TypeScript.
- 1
Copy the example
cp -R plugins/hello-world plugins/acme-seo - 2
Set the id
Use a namespaced id in
justflows.jsonandsrc/index.ts(acme.seo). Declare a GPL-compatiblelicense. - 3
Write and build
Edit
src/. Thenpnpm --filter acme.seo buildsodist/index.jsexists.
Note
Do not put plugin source under packages/. That tree is platform code. Site owners never copy folders into plugins/ — they install a .jfpkg in Admin.
Install a packaged plugin
Admin → Plugins. Drop a .jfpkg on the page or use justflows plugin install <path>. Then activate. Core is MIT; plugins keep the license in their manifest (example: GPL-2.0-or-later).
Lifecycle APIs
| Action | HTTP | CLI |
|---|---|---|
| List | GET /api/plugins | justflows plugin list |
| Install | POST /api/plugins | justflows plugin install <path> |
| Activate | POST /api/plugins/:id/activate | justflows plugin activate <id> |
| Deactivate | POST /api/plugins/:id/deactivate | justflows plugin deactivate <id> |
| Delete | DELETE /api/plugins/:id | — |
Actions fired: plugin.installed, plugin.activated, plugin.deactivated, plugin.uninstalled. Runtime loads active plugins in plugin-runtime before deferred routes register.
Minimal plugin
import type { PluginModule } from "@justflows/sdk";
const plugin: PluginModule = {
manifest: {
id: "acme.welcome",
name: "Acme Welcome",
version: "1.0.0",
license: "GPL-2.0-or-later",
permissions: [],
main: "index.js",
},
activate(ctx) {
ctx.hooks.action("content.published", (event) => {
ctx.logger.info("Published", { contentId: event.contentId });
});
},
};
export default plugin;Manifest rules
- id — dot-separated namespace, e.g.
acme.my-plugin. - version — semver.
- license — required; the plugin's own license. Must be a GPL-compatible SPDX identifier (GPL, MIT, BSD, ISC, …). Not inherited from MIT core.
- permissions — declared up front; sensitive ones are
network:outbound,users:manage,settings:manage,auth:hook. - main — entry file, default
index.js. - Optional
minJustflowsVersion/maxJustflowsVersion, description, author, homepage.
Plugin permissions
content:read|create|update|delete|publish, media:read|upload|delete, users:read|manage, settings:read|manage, network:outbound, admin:extend, jobs:register, auth:hook.
Context (`activate(ctx)`)
hooks— typed action / gate / filter registration (see Hooks).cache— namespaced jf-cache (plugin:{id}:…).remember,get,set,delete,invalidate.settings— plugin settings store.logger— attributed to your plugin id.pluginId,version,permissionsset.
Good to know
You do not unregister hooks on deactivate — the loader disposes registrations when the plugin stops. Import types from @justflows/sdk only.