Overview
OpeNext plugins are installed from a ZIP via Dashboard → Plugins. Packages can be frontend-only (HTML, CSS, JS in the browser) or full-stack (frontend + server/ 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 Development docs.
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
└── server/
├── index.js 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.cssStep 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 a server/ 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": "server/index.js",
"migrations": ["server/migrations"],
"permissions": ["crm.read", "crm.write"],
"installHook": "server/hooks/install.js"
}// 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
Compile frontend source before zipping. Backend server files must be plain JavaScript.
// 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
- Compile server/ TypeScript to .js if needed
- 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
- Frontend → public/plugins/{id}/; backend → plugins/{id}/
- Migrations and install hook run automatically for full-stack plugins
- Plugin is installed and enabled automatically
- 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 frontend before upload — TypeScript/JSX source will not run in the browser
- Use plugin-data API for small JSON; use server/ routes for relational data
- Do not register backend routes at /data/* — reserved for CMS plugin-data API
Full Plugin Development docs → · Contributing guide → · Examples on GitHub →