All tutorials

OpeNext CMS · Plugins

Plugin Development Guide

Build block, dashboard, hybrid, and full-stack plugins — manifest format, backend APIs, and ZIP upload workflow.

15 min readPublished 2026-06-15

Overview

OpeNext plugins are installed from a ZIP via Dashboard → Plugins. Packages can be frontend-only (HTML, CSS, JavaScript, or TypeScript in the browser) or full-stack (frontend + api/ folder with REST APIs, models, and migrations).

Frontend plugin kinds: block plugins for the page editor, system (dashboard) plugins for admin apps, and hybrid plugins that do both.

For the full reference (manifest fields, upload lifecycle, security), read the Plugin Upload docs.

JavaScript or TypeScript

You can write a plugin in JavaScript or TypeScript. Upload .js files directly, or reference .ts and .tsx files from the manifest. OpeNext CMS transpiles those TypeScript files to JavaScript during upload.

// manifest.json
{
  "id": "crm-plugin",
  "name": "CRM",
  "version": "1.0.0",
  "kind": "system",
  "adminEntry": "ui/admin/index.html",
  "serverEntry": "api/index.ts",
  "blockEntry": "ui/block/index.ts"
}
crm-plugin/
└── crm-plugin/
    ├── manifest.json
    ├── ui/
    │   ├── admin/
    │   │   ├── index.html
    │   │   └── app.ts
    │   └── block/
    │       └── index.ts
    └── api/
        ├── index.ts
        └── routes/
            └── customers.ts
  • Use .js for a plugin with no compile step
  • Use .ts or .tsx for type safety; the original source is retained and JavaScript runtime files are generated
  • Simple plugin modules, routes, services, models, hooks, and blocks can use TypeScript
  • Build React/Vue applications before upload because the uploader transpiles but does not bundle npm imports

What files you need

Block plugin (page editor only)
my-block/
├── manifest.json     Required
├── index.js          Required — mount script
└── style.css         Optional

System / dashboard plugin (frontend-only)
my-crm/
├── manifest.json     Required
└── admin/
    ├── index.html    Required — iframe entry
    ├── app.js        Recommended
    └── style.css     Optional

Full-stack system plugin (frontend + backend)
my-crm/
├── manifest.json
├── admin/
│   └── index.html
└── api/
    ├── index.js or index.ts  Required — backend entry
    ├── routes/       REST route files (auto-loaded)
    ├── models/       Mongoose schemas
    ├── migrations/   Database migrations
    └── hooks/        install.js / uninstall.js

Hybrid (dashboard + page block)
my-plugin/
├── manifest.json
├── admin/
│   └── index.html
└── block/
    ├── index.js
    └── style.css

Step 1 — Block plugin from scratch

Create a testimonial block editors can add to any page.

// manifest.json
{
  "id": "testimonial-block",
  "name": "Testimonial Block",
  "version": "1.0.0",
  "kind": "block",
  "type": "testimonial",
  "icon": "💬",
  "entryPoint": "index.js",
  "styles": ["style.css"]
}
// index.js
window.__NEXTCMS_PLUGINS__ = window.__NEXTCMS_PLUGINS__ || {};

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

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

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

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

The registry key testimonial-block must exactly match id in the manifest.

Step 2 — Dashboard (system) plugin

Build a CRM-style admin app that opens in the dashboard sidebar.

// manifest.json
{
  "id": "crm-plugin",
  "name": "CRM",
  "version": "1.0.0",
  "kind": "system",
  "type": "crm",
  "icon": "👥",
  "adminEntry": "admin/index.html"
}

For small JSON storage without a backend, use the plugin-data API from admin/app.js:

fetch('/api/plugins/crm-plugin/data/contacts', { credentials: 'include' })
  .then(function (res) { return res.json(); })
  .then(function (payload) {
    var contacts = payload.data?.data || [];
    // render contacts
  });

The CMS loads admin/index.html in a sandboxed iframe. Always use relative asset paths (./app.js).

Step 3 — Full-stack backend plugin

Add an api/ folder for REST APIs, database models, and migrations. APIs are mounted at /api/plugins/{pluginId}/* automatically.

// manifest.json (backend fields)
{
  "id": "crm-plugin",
  "name": "CRM",
  "version": "1.0.0",
  "kind": "system",
  "adminEntry": "admin/index.html",
  "serverEntry": "api/index.ts",
  "migrations": ["api/migrations"],
  "permissions": ["crm.read", "crm.write"],
  "installHook": "api/hooks/install.ts",
  "dependencies": ["pdfkit"]
}

Runtime npm packages (for example pdfkit) are listed in dependencies. Upload merges them into the CMS root package.json; run npm install at the CMS root, restart, then enable the plugin.

// server/index.js
module.exports = {
  register({ app, cms }) {
    cms.logger.info('CRM backend registered');
  },
  boot({ cms }) {},
  shutdown() {}
};

// server/routes/customers.js — auto-loaded
module.exports = (app) => {
  app.get('/customers', async (req, res) => {
    res.json({ customers: [] });
  });
};
// admin/app.js — call your plugin API
fetch('/api/plugins/crm-plugin/customers', { credentials: 'include' })
  .then(function (res) { return res.json(); })
  .then(function (payload) {
    // render payload.customers
  });

On upload, the CMS runs migrations, the install hook, registers permissions, and loads routes. Handlers receive req.user and req.cms automatically.

Step 4 — Hybrid plugin

Add a page-editor block alongside your dashboard app.

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

Step 5 — Build with React or Vite

Build frontend applications before zipping. Simple plugin TypeScript files are compiled automatically during upload.

// vite.config.ts
export default defineConfig({
  base: './',
  build: {
    outDir: 'crm-plugin/admin',
    emptyOutDir: true
  }
});
  • Run npm run build to produce admin/index.html + hashed JS/CSS
  • Copy manifest.json into the output folder
  • Use .ts files directly for simple api/ modules, routes, services, models, migrations, and hooks
  • List backend npm deps in manifest.json dependencies — upload merges them into CMS package.json
  • Do not include node_modules/, src/, or .env in the ZIP

Step 6 — Package and upload

# Zip contents so manifest.json is at the root
Compress-Archive -Path .\crm-plugin\* -DestinationPath .\crm-plugin.zip -Force
  • Dashboard → Plugins → Upload Plugin
  • Upload merges manifest dependencies into CMS root package.json
  • Run npm install at CMS root, restart, then enable the plugin
  • To update: remove old plugin, bump version, re-upload with same id

Best practices

  • Keep the plugin id stable — it is the registry key and API namespace
  • Use textContent for user data — avoid unsafe innerHTML
  • Never embed secrets in browser files
  • Use credentials: 'include' for all fetch calls to CMS APIs
  • Build React/Vue frontend applications before upload — the uploader transpiles simple TypeScript but does not bundle JSX imports
  • Use plugin-data API for small JSON; use api/ routes for relational data
  • Do not register backend routes at /data/* — reserved for CMS plugin-data API

Full Plugin Upload docs → · Contributing guide → · Examples on GitHub →