OpeNext CMS · Developer Docs

Plugin Development

Build and upload plugin packages for OpeNext CMS — frontend-only blocks and dashboard apps, or full-stack plugins with REST APIs, models, and migrations from a single ZIP.

How plugins work

Plugins are installed from a single ZIP via Dashboard → Plugins → Upload Plugin. A package can be:

  • Frontend-only — static HTML, CSS, and JavaScript served from /plugins/{pluginId}/
  • Full-stack — frontend assets plus optional server/ backend code with REST APIs under /api/plugins/{pluginId}/*

Frontend plugins load in one of three shapes:

  • Block plugins — mount into the page editor and live site via window.__NEXTCMS_PLUGINS__
  • System (dashboard) plugins — admin UI runs in a sandboxed iframe at /dashboard/plugins/runtime/{pluginId}
  • Hybrid plugins — both a dashboard app and an optional page-editor block

After upload, the CMS stores plugin metadata in MongoDB. Frontend files land in public/plugins/{pluginId}/. Backend code (if present) is extracted to plugins/{pluginId}/ and is never served as static files or executed from public/.

Package types at a glance

| Type | Contains | Backend APIs | Best for |
| ---- | -------- | ------------ | -------- |
| Block plugin | manifest + index.js | No | Page-editor widgets |
| System plugin | manifest + admin/ | No (plugin-data API only) | Simple dashboard apps |
| Hybrid plugin | admin/ + block/ | No | Dashboard + canvas block |
| Full-stack system | admin/ + server/ | Yes — /api/plugins/{id}/* | CRM, inventory, custom APIs |

Dashboard plugin and system plugin mean the same thing — a full admin application opened from the dashboard sidebar.

Choose your plugin type

| Type | kind in manifest | Required files | Where it runs |
| ---- | ---------------- | -------------- | ------------- |
| Block plugin | "block" | manifest.json + index.js | Page editor palette + public pages |
| System / dashboard plugin | "system" | manifest.json + admin/index.html | Dashboard iframe |
| Hybrid plugin | "system" + blockEntry | manifest.json + admin/ + block/ | Dashboard iframe + page editor |
| Full-stack plugin | "system" + serverEntry | manifest.json + admin/ + server/index.js | Dashboard + /api/plugins/{id}/* |

Files you need (by type)

| File / folder | Block | System | Hybrid | Full-stack | Purpose |
| ------------- | ----- | ------ | ------ | ---------- | ------- |
| manifest.json | Yes | Yes | Yes | Yes | Plugin identity, kind, entry paths |
| index.js | Yes | — | — | — | Block mount script (default entryPoint) |
| admin/index.html | — | Yes | Yes | Yes | Dashboard iframe entry |
| admin/app.js | — | Rec. | Rec. | Rec. | Dashboard application logic |
| block/index.js | — | — | Yes* | Optional | Page-editor block (*hybrid only) |
| server/index.js | — | — | — | Yes** | Backend entry (**or manifest.serverEntry) |
| server/routes/ | — | — | — | Optional | Auto-loaded REST route files |
| server/models/ | — | — | — | Optional | Mongoose model definitions |
| server/migrations/ | — | — | — | Optional | Database migration scripts |
| server/hooks/ | — | — | — | Optional | install.js / uninstall.js |
| assets/, fonts/ | Optional | Optional | Optional | Optional | Images, icons, web fonts |

Always required in manifest.json: name, version
Strongly recommended: stable id (never change between releases)

Step 1 — Create manifest.json

Every plugin starts with manifest.json at the package root. The installer reads this first to determine plugin ID, kind, entry paths, and optional backend configuration.

{
  "id": "crm-plugin",
  "name": "CRM",
  "version": "1.0.0",
  "description": "Customer relationship management",
  "author": "Your Company",
  "kind": "system",
  "type": "crm",
  "icon": "👥",
  "adminEntry": "admin/index.html",
  "blockEntry": "block/index.js",
  "styles": ["block/style.css"],
  "serverEntry": "server/index.js",
  "migrations": ["server/migrations"],
  "models": ["server/models"],
  "permissions": ["crm.read", "crm.write"],
  "installHook": "server/hooks/install.js",
  "uninstallHook": "server/hooks/uninstall.js",
  "dependencies": []
}
  • id — 3–64 chars, lowercase letters/numbers/-/_; must match __NEXTCMS_PLUGINS__ registry key
  • name — required display name
  • version — required semver string
  • kind — block or system (auto-detected from admin/ folder if omitted)
  • type — category label (crm, chart, slider) — not the same as kind
  • adminEntry — HTML for dashboard plugins (defaults: admin/index.html, dashboard/index.html)
  • entryPoint — JS for block plugins (defaults to index.js)
  • blockEntry — JS for hybrid system plugins (defaults to block/index.js)
  • serverEntry — backend main module (defaults to server/index.js when present)
  • migrations — migration directories (defaults to server/migrations/)
  • permissions — permission strings registered on install
  • installHook / uninstallHook — scripts run after migrations / during uninstall
  • styles — CSS loaded before block scripts (paths must exist in ZIP)

Step 2 — Develop a block plugin

Block plugins add a component editors drag from the page-editor plugin palette onto the canvas. The block type in the editor equals your plugin id.

testimonial-block/
├── manifest.json
├── index.js              Required — block entry script
├── style.css             Optional
└── assets/
    └── quote.svg
window.__NEXTCMS_PLUGINS__ = window.__NEXTCMS_PLUGINS__ || {};

window.__NEXTCMS_PLUGINS__['testimonial-block'] = {
  mount(element, context) {
    const data = (context.block || {}).data || {};
    element.innerHTML = '';

    const quote = document.createElement('blockquote');
    quote.textContent = data.quote || 'Your testimonial';

    const author = document.createElement('p');
    author.textContent = data.author || 'Customer name';

    element.append(quote, author);
  },
  unmount(element) {
    element.innerHTML = '';
  }
};

mount(element, context) receives { block: {}, isEditing: true }. Use textContent for user content — avoid unsafe innerHTML.

Step 3 — Develop a system (dashboard) plugin

System plugins are full dashboard applications. The CMS opens your HTML in an iframe at /dashboard/plugins/runtime/{pluginId} and adds a sidebar link.

crm-plugin/
├── manifest.json
└── admin/
    ├── index.html        Required — iframe entry point
    ├── app.js            Your dashboard application
    └── style.css         Dashboard styles

Use relative URLs in admin HTML (./app.js, ./style.css). Installed assets are served at /plugins/{pluginId}/{path}.

Option A — plugin-data API (frontend-only, no server/ folder). Good for small JSON payloads up to 256 KB per key:

const pluginId = 'crm-plugin';

const response = await fetch('/api/plugins/' + pluginId + '/data/contacts', {
  credentials: 'include'
});
const payload = await response.json();
const contacts = payload.data?.data || [];

Option B — full-stack plugin APIs (with server/ folder). Good for CRUD, relational data, and migrations — see Step 5.

Step 4 — Develop a hybrid plugin

A hybrid plugin combines a dashboard app with a page-editor block — for example, a CRM admin panel plus a contact-summary widget on pages.

{
  "id": "crm-plugin",
  "name": "CRM",
  "version": "1.0.0",
  "kind": "system",
  "type": "crm",
  "icon": "👥",
  "adminEntry": "admin/index.html",
  "blockEntry": "block/index.js",
  "styles": ["block/style.css"]
}

The dashboard loads adminEntry in the iframe; the page editor loads blockEntry through the same __NEXTCMS_PLUGINS__ mount contract.

Step 5 — Add a backend (full-stack plugin)

Add a server/ folder when your plugin needs REST APIs, database tables, or install/uninstall hooks. One ZIP installs everything — no CMS source changes required.

crm-plugin/
├── manifest.json
├── admin/
│   └── index.html
└── server/
    ├── index.js              Main server module (required)
    ├── routes/
    │   └── customers.js      Auto-loaded route files
    ├── services/
    │   └── CustomerService.js
    ├── models/
    │   └── Customer.js       Mongoose schemas
    ├── migrations/
    │   └── 001_create_customers.js
    └── hooks/
        ├── install.js        Runs after migrations
        └── uninstall.js      Runs on uninstall

Every backend plugin receives a dedicated API namespace. Define routes relative to your plugin — the CMS mounts them automatically:

GET    /api/plugins/crm-plugin/customers
POST   /api/plugins/crm-plugin/customers
PUT    /api/plugins/crm-plugin/customers/:id
DELETE /api/plugins/crm-plugin/customers/:id

server/index.js exports a server module contract:

module.exports = {
  register({ app, cms }) {
    // Optional: register routes manually
    app.get('/health', (req, res) => {
      res.json({ ok: true, plugin: cms.pluginId });
    });
  },
  boot({ cms }) {
    cms.logger.info('CRM plugin booted');
  },
  shutdown() {}
};

Route files in server/routes/*.js are auto-loaded. Each file may export a function that receives the plugin router, or an array of route definitions.

// server/routes/customers.js
module.exports = (app) => {
  app.get('/customers', async (req, res) => {
    const cms = req.cms;
    const Customer = cms.models.get('Customer');
    const customers = await Customer.find({}).lean();
    res.json({ customers });
  });
};

Plugin APIs inherit CMS authentication automatically. Handlers receive req.user, req.permissions, and req.cms (the plugin SDK with database, models, logger, and permissions).

Call plugin APIs from your admin UI:

const response = await fetch('/api/plugins/crm-plugin/customers', {
  credentials: 'include'
});
const { customers } = await response.json();

Migrations in server/migrations/ run once during install and are tracked in the plugin_migrations collection. The path segment data is reserved for the CMS plugin-data API — do not register backend routes at /data/*.

Plugin data API (frontend-only storage)

Uploaded and enabled plugins can store authenticated JSON (max 256 KB per key) without a server/ folder:

  • GET /api/plugins/{pluginId}/data — list all keys
  • GET /api/plugins/{pluginId}/data/{key} — read one key
  • POST /api/plugins/{pluginId}/data — create or update: { key, data }
  • PUT /api/plugins/{pluginId}/data/{key} — replace one key
  • DELETE /api/plugins/{pluginId}/data/{key} — delete one key

For large or relational data, use a full-stack plugin with dedicated routes under /api/plugins/{pluginId}/* instead.

Building with React or Vite

React, Vue, Svelte, or TypeScript frontend source must be compiled before upload. The ZIP must contain browser-ready output in admin/ and block/.

// vite.config.ts
export default defineConfig({
  base: './',
  build: {
    outDir: 'crm-plugin/admin',
    emptyOutDir: true
  }
});

For backend plugins, compile any TypeScript server code to plain JavaScript before zipping. The server/ folder must contain runnable .js files.

Never include node_modules/, src/, .env, or private keys in the ZIP.

Package and upload

Zip the contents of your plugin folder so manifest.json is at the ZIP root, or zip one wrapper folder. Both layouts work.

# PowerShell
Compress-Archive -Path .\crm-plugin\* -DestinationPath .\crm-plugin.zip -Force
  • Open Dashboard → Plugins → Upload Plugin
  • Select or drag your .zip (max 25 MB, max 200 files)
  • Plugin is installed and enabled automatically
  • System plugins — open from dashboard sidebar or Plugins → Open
  • Block plugins — add from page-editor plugin palette
  • Duplicate plugin ID returns 409 — remove the old plugin first

On failure, extracted files and partial database rows are rolled back.

Upload lifecycle (what actually happens)

Upload ZIP
  → Validate size (25 MB) and file count (200)
  → Read and validate manifest.json
  → Extract frontend → public/plugins/{pluginId}/
  → Extract backend  → plugins/{pluginId}/          (if server/ present)
  → Run pending database migrations                 (if backend)
  → Run install hook                                (if backend)
  → Register manifest permissions                   (if backend)
  → Load server module + routes                     (if backend)
  → Save plugin metadata in MongoDB (isActive: true)
  → Enable plugin

Block plugin at render time:
  → Load styles[] then entryPoint script
  → Call window.__NEXTCMS_PLUGINS__[id].mount(element, { block, isEditing })

System plugin when opened:
  → Navigate to /dashboard/plugins/runtime/{id}
  → Load adminEntry in sandboxed iframe
  → Admin JS calls plugin APIs or plugin-data API with credentials: include

CMS startup (backend plugins):
  → Bootstrap loads all active plugins with hasBackend: true
  → Registers routes under /api/plugins/{pluginId}/*

Enable, disable, and uninstall

| Action | Frontend | Backend |
| ------ | -------- | ------- |
| Disable | Hidden from editor/sidebar | Routes unloaded; database kept |
| Enable | Visible again | Server module and routes reloaded |
| Uninstall | Files removed from public/plugins/{id}/ | server/ removed; uninstall hook runs |

Uninstall also deletes plugin-data records. Migration history is kept by default so reinstalling does not re-run migrations.

Security and limits

  • ZIP size: 25 MB; max 200 files
  • Blocked extensions: .exe, .dll, .bat, .cmd, .sh, .ps1, .msi, .com, .scr
  • Blocked paths: .env, node_modules/, .git/ — never extracted
  • Frontend files → public/plugins/{id}/; backend → plugins/{id}/ only
  • Backend code never executed from public/
  • Plugin browser JS runs same-origin — treat as trusted code
  • Never embed secrets or API keys in plugin files

Common errors

  • manifest.json was not found — place it at ZIP root or one wrapper folder deep
  • System plugin requires admin/index.html — add admin/index.html or set adminEntry
  • Block plugin requires an entryPoint — add index.js or declare entryPoint in manifest
  • Backend plugin requires server/index.js — add server/index.js or set serverEntry
  • Block script loads but nothing renders — registry key must exactly match manifest id
  • Plugin is already installed — remove existing plugin before uploading an update
  • Plugin API returns 401 — call with credentials: include while logged into CMS
  • Plugin API returns 503 — plugin disabled or backend failed to load; check server logs
  • Migration failed during upload — fix server/migrations/*.js; upload rolls back on failure

Related guides

Walk through a full example in the Plugin Development tutorial. Reference implementation with backend APIs: examples/crm-plugin/ in the CMS repository.