JustFlows

Plugin development

Build a third-party plugin

A plugin is an installable ESM module that activates inside Justflows through a permission-scoped context. It can react to lifecycle events, add UI and blocks, store data, expose routes, and integrate external services.

Start from the supported example

Copy plugins/hello-world in the CE repository. The runtime loads dist/index.js or index.js; it does not execute TypeScript from src.

Project structure
acme-plugin/
├── justflows.json
├── package.json
├── src/
│   ├── index.ts
│   └── styles/plugin.css
└── dist/
    ├── index.js
    └── styles/plugin.css
src/index.ts
import type { PluginModule } from "@justflows/sdk";

const plugin: PluginModule = {
  manifest: {
    id: "acme.seo",
    name: "Acme SEO",
    version: "1.0.0",
    license: "GPL-2.0-or-later",
    engines: { justflows: ">=0.1.8 <0.2.0" },
    permissions: ["content:read"],
    main: "index.js",
  },

  activate(ctx) {
    ctx.hooks.action("content.published", ({ contentId }) => {
      ctx.logger.info("Content published", { contentId });
    });
  },

  async deleteData(ctx) {
    await ctx.data.clear();
  },
};

export default plugin;

The plugin context

Hooks & capabilities

ctx.hooks registers actions, gates, and filters. ctx.capabilities adds plugin-owned user capabilities while active.

Storage & secrets

ctx.data stores plugin-scoped JSON; ctx.secrets stores encrypted credentials; ctx.databases manages prefixed tables.

HTTP, jobs & mail

ctx.http exposes /ext/&lbrace;pluginId&rbrace; routes. Jobs and mail transports require their manifest permissions.

Content & blocks

ctx.content idempotently creates types/pages. ctx.blocks registers namespaced server-rendered blocks.

Settings & cache

ctx.settings stores small plugin settings. ctx.cache is namespaced and supports read-through caching.

Cookies

ctx.cookies.declare() registers non-essential cookies so consent UI can disclose and enforce them.

Permissions are least-privilege

Declare only what you use. The host uses the manifest to decide which context APIs are available and highlights sensitive access during installation.

content:readcontent:createcontent:updatecontent:deletecontent:publishcontent:revisions:readcontent:revisions:restorecontent:revisions:discardmedia:readmedia:uploadmedia:deleteusers:readusers:managesettings:readsettings:manageadmin:extendjobs:registerauth:hooknetwork:outboundmail:transportmail:templatesmail:hook

Add an admin page

Request admin:extend and declare adminMenu. Paths must live below /admin/. A setupPath can open a first-run wizard after activation.

justflows.json
{
  "permissions": ["admin:extend"],
  "setupPath": "/admin/acme-seo",
  "adminMenu": [{
    "id": "seo",
    "label": "SEO",
    "path": "/admin/acme-seo",
    "icon": "🔎",
    "domain": "extensions"
  }]
}

Styles that follow the active theme

Read and cache your built stylesheet during activation, then append it through the async theme.css filter. Namespace classes and use public theme variables such as --color-*, --space-*, and --radius-*.

typescript
ctx.hooks.filter("theme.css", (current) =>
  current.includes(MARKER)
    ? current
    : `${current}\n${MARKER}\n${stylesheet}`
);

Cleanup and packaging

Every plugin implements deleteData. Remove plugin data, owned tables, and optionally declared content types according to operator settings. Build before packaging, because the host will not install or compile anything.

Package from the plugin directory
pnpm build
COPYFILE_DISABLE=1 tar -czf ../../acme-seo.jfpkg justflows.json dist