Publishing a plugin
Installing, updating and rolling back work in the web UI (Plugins, tab Installed) and through the REST API. Publishing the SDK to npm and a plugin template repository are milestone M6. How to install is described in Installation.
What exists today
- The package format below (the
loxNexusmanifest and a self-contained ESM bundle). - Installing from a ZIP file, a git repository or the server's local plugin repository.
- Development loading: Nexus runs every plugin in the directory named by
NEXUS_DEV_PLUGINS_DIRfrom source (src/index.ts) and restarts its instances when a file changes.npm run devpoints it at the repository'splugins/directory. Because the SDK is a workspace package and is not on npm yet, a plugin is developed inside a Nexus checkout, inplugins/<name>/, and can later be moved out. The tutorial walks through it.
The manifest
A plugin is an npm package whose package.json has a loxNexus block:
{
"name": "nexus-plugin-example",
"version": "1.2.0",
"type": "module",
"loxNexus": {
"apiVersion": 1,
"id": "example",
"entry": "dist/plugin.js",
"name": "Example",
"icon": "plug"
}
}
| Field | Rules |
|---|---|
apiVersion |
1. The version of this manifest format, not of the SDK: a Nexus refuses a format it can't read. The protocol your bundle speaks comes from the SDK it was built with; Nexus checks that at every start and says whether to update Nexus or the plugin. |
id |
The plugin's topic segment and stable reference (example/<instance>/sensor/...). Lowercase letters, digits, ., _ and -, starting with a letter or digit. loxone and system are reserved for Nexus. Never change it after release: instances and topics refer to it. |
name |
Display name, not empty. |
entry |
The built bundle, relative to the package. In development src/index.ts runs instead. To install: a relative path such as dist/plugin.js (no leading / or ./, no .., backslashes or empty segments), at most 512 characters, ending in .js or .mjs. |
heapMb |
Optional: the worker's heap limit in MB, 128 to 512 (default 128), for plugins that need more, such as a Matter stack. The install review shows it as "needs up to N MB of memory per instance". |
icon |
Optional icon shown in the UI: bell, calendar, camera, cloud, code, database, gauge, home, lightbulb, music, plug, solar (or sun), thermometer, webhook, wifi or zap. Other names, or none, show a plug. |
version (top level) |
Defaults to 0.0.0 when absent. To install: required, a strict semantic version such as 1.2.0 or 1.3.0-beta.1, at most 64 characters. |
type (top level) |
To install: "module", unless the entry ends in .mjs. A plugin is an ES module. |
To install, the manifest rules are stricter than for development. The id is at most 64
characters (it is also a folder name), name at most 100, and icon at most 50 (lowercase letters,
digits and -). Nexus names the field that is wrong when it refuses a package.
Building the bundle
A plugin ships a prebuilt, self-contained ESM bundle, so installing never runs npm. Build it
with esbuild, as the first-party plugins do:
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node24 --conditions=@lox-nexus/source --outfile=dist/plugin.js"
}
--conditions=@lox-nexus/source makes the workspace SDK resolve to its TypeScript source. Once the
SDK is published, a plugin outside the repository drops that flag and bundles the published
package instead.
What may be bundled
-
Bundled in: the SDK, zod and any pure JavaScript dependency (an iCal parser, a client library). The smart plug of the tutorial comes to about 850 KB because zod is included.
-
Not possible: native modules (
.nodeaddons) and anything that needsnpm installat run time. Mark nothing as--external: the bundle must be a single file, with nonode_modulesnext to it. -
Node built-ins (
node:fs,node:net, ...) are available. -
CommonJS dependencies (an MQTT client, for example) make esbuild write
__require("events")into the ESM bundle, which fails when Nexus loads it withDynamic require of "stream" is not supported. Definerequirewith a banner in the build script:"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node24 --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --outfile=dist/plugin.js"This works because Nexus loads the entry by file URL, so
import.meta.urlis the bundle's own path. Before you publish, load the built bundle once from a small.mjs(await import("./dist/plugin.js")) to catch this error early. -
The code runs in a worker with an empty environment (no
process.envvalues) and a 128 MB heap, or theheapMbits manifest asks for.
Releasing
- Raise
version, build the bundle, and make it part of what you hand out:package.jsonand the file named byentryare what gets installed. A source map next to the bundle (<entry>.map) is installed too if there is one, and so is the README (see below). Everything else is ignored: sources,node_modulesand other files are not installed.- Write a README for the people who install the plugin. Put
README.md(orreadme.md,Readme.md) at the package root. Nexus keeps it, from a ZIP, a git repository and the local repository alike, and the Installed page shows it rendered as Markdown under Read me on the plugin's card. Say what the plugin does, its settings, its sensors and actuators, and its limits. A README larger than 512 KiB is left out and the install goes on. Raw HTML is shown as text, images show only their alt text, and links open in a new tab.
- Write a README for the people who install the plugin. Put
- Hand it out in one of two ways.
- A ZIP file.
package.jsonis at the archive's root or in its only top-level folder (as "download as ZIP" and most zip tools pack a folder). Up to 50 MiB; no absolute paths,.., backslashes, symbolic links, encrypted or duplicate entries, and at most 5 000 entries. - A git repository. An
https://URL (no token or user name in it) and a branch or tag. Commit the built bundle to the repository, since installing never runsnpm. Git LFS isn't fetched, so keep the bundle a normal file.
- A ZIP file.
- An admin installs it in the UI: Nexus fetches and checks the package and starts the bundle once in a throwaway worker to see what it offers, shows the impact on the review step, and installs it after confirmation. See Installation.
The same version with the same bundle counts as already installed, and confirming it changes
nothing. The same version with a different bundle is refused, so raise the version whenever
the bundle changes. Only the local plugin repository stamps versions itself: it installs 1.2.0 as
1.2.0+<hash of the bundle and the README>, so a rebuilt plugin, or one whose README changed, installs there as a new version. A different version is an update: running instances restart on it (stopped, then started as a new worker, see the lifecycle), and the
version before stays for a rollback.
Keep stored data compatible. Updates and rollbacks don't migrate an instance's config or store. Read the formats older versions of your plugin wrote, and tolerate fields a newer version added, so a rollback keeps working. A required setting that is new or was optional before stops existing instances with an error until the user fills it in; give new settings a default where you can.
License
The plugin SDK is under the MIT License, and your plugin only depends on the SDK and talks to
Nexus over messages. So you choose your plugin's license freely, closed source included; Nexus
itself is GPL-3.0-or-later, which doesn't extend to plugins. The first-party plugins are MIT as
well, so you may copy from them. Put a LICENSE file and the license field in package.json,
so users can see what they install.
Trust
Plugins are trusted code. Each runs in a worker thread with an empty environment and a memory
limit, but worker threads are not a sandbox: a plugin can read files, open network
connections and use anything Node offers to the Nexus process. Installing is admin-only (every user is an admin in v1) and records where each version came from (the git
commit, or the ZIP's checksum). Staging a plugin already runs its code once. Only run plugins whose
code you trust, and keep secrets in secret() config fields: they never reach the browser or the bus.