Standalone Plugins

Develop a plugin in its own repository, run it against a local erxes checkout, and deploy it as a separate API service and UI remote.

Monorepo or separate repository

Create a Plugin adds paired _api and _ui projects inside the erxes monorepo. A standalone plugin uses the same platform mechanisms from outside it: the API registers with the gateway through Redis service discovery and joins the federated supergraph, and the UI is a Module Federation remote that Core UI loads from the URL the API announces. Nothing in the erxes repository references the plugin except its entry in ENABLED_PLUGINS.

Choose a separate repository when the plugin has its own release cycle, its own team, or source code that does not belong in the erxes repository. Choose the monorepo when the plugin is part of erxes itself.

Prerequisites

  • An erxes checkout on main running locally per Local Setup. Loading remotes from outside the monorepo in development and the packaged shared libraries are on main and part of the next release.
  • Node.js 22. pnpm is the recommended package manager for the plugin repository: it resolves git dependencies on monorepo subdirectories, which the shared libraries rely on.
  • MongoDB and Redis reachable from the plugin, using the same instances as your erxes checkout.

Install the CLI

create-erxes-plugin is a standalone binary; it does not need Node.js to run.

# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/erxes/create-erxes-plugin/main/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/erxes/create-erxes-plugin/main/install.ps1 | iex

The installer picks /usr/local/bin when writable, otherwise ~/.local/bin; on Windows it uses %LOCALAPPDATA%\Programs\create-erxes-plugin and adds it to the user PATH. Pass --version <release> and --dir <path> after sh -s -- to pin a release or choose the directory. With Node.js installed, npx create-erxes-plugin runs the same CLI. The source is at erxes/create-erxes-plugin.

Generate and connect a plugin

  1. Generate the repository

    create-erxes-plugin inventory --backend platform --pm pnpm
    

    The CLI asks for anything not passed as a flag:

    FlagDefault
    [directory]the plugin name
    -n, --name <name>the directory name
    -t, --title <title>the name in Title Case, shown in erxes navigation
    -d, --description <text><title> plugin for erxes
    -b, --backend <express|platform>asked interactively
    --pm <npm|pnpm|yarn|bun>the package manager running the CLI
    --api-port <port>3399
    --ui-port <port>3099
    --erxes-ref <ref>main; branch, tag, or commit of erxes/erxes for the shared libraries
    --no-install, --no-gitdependencies are installed and git init runs
    -y, --yesaccept defaults for every unanswered question

    Plugin names are lowercase words separated by single dashes (inventory, erxes-agent-v2). The generated repository:

    erxes.json                 plugin identity and ports, read by api/ and ui/
    api/                       the plugin API (see the next step)
    ui/                        React Module Federation remote with prefixed Tailwind CSS
    Dockerfile                 API-only image for the chosen package manager
    README.md
    docs/erxes-integration.md  how this plugin plugs into erxes, with its names filled in
    
  2. Pick the backend stack

    StackWhat you getRuntime
    platformstartPlugin from erxes-api-shared: gateway registration, /health, /graphql, gateway-header parsing, tenant-scoped Mongoose models through createGenerateModels, schemaWrapper, and checkPermission on the context. The same architecture as a monorepo plugin.Node.js
    expressExpress with an Apollo Server federated subgraph and no dependency on erxes-api-shared. api/src/gateway.ts writes the same Redis keys joinErxesGateway writes, and api/src/context.ts turns the gateway's forwarded headers into the GraphQL context.Node.js or Bun

    Use platform for plugins that store tenant data or call core modules; express suits thin integrations that only need to appear in the supergraph. Either way, every GraphQL type and operation carries the plugin prefix (Inventory, inventory) because all plugins share one supergraph. See Backend Plugins for the startPlugin options and GraphQL & tRPC for subgraph conventions.

  3. Understand the shared-library dependencies

    erxes-ui, ui-modules, and erxes-api-shared live in the erxes monorepo (frontend/libs, backend/erxes-api-shared) and are not on npm yet. The generated manifests consume them as git dependencies on monorepo subdirectories:

    // ui/package.json
    "erxes-ui":   "github:erxes/erxes#main&path:frontend/libs/erxes-ui",
    "ui-modules": "github:erxes/erxes#main&path:frontend/libs/ui-modules"
    
    // api/package.json (platform stack)
    "erxes-api-shared": "github:erxes/erxes#main&path:backend/erxes-api-shared"
    
    • The ref after # (--erxes-ref) is what the plugin compiles and type-checks against. Pin it to the commit your erxes deployment runs: at runtime Core UI provides erxes-ui and ui-modules as Module Federation singletons, so a mismatch shows up as broken imports rather than a helpful error.
    • On install, each library runs its prepare script: erxes-api-shared builds its bundles and the UI libraries emit type declarations into dist/. TypeScript then checks the plugin against those declarations only. The generated pnpm-workspace.yaml lists the three packages under onlyBuiltDependencies because pnpm blocks dependency build scripts by default. Expect the first install to take a few minutes; it resolves the erxes workspace once and caches it.
    • With npm, yarn, or bun the CLI writes file:../erxes/... specifiers instead, which expect an erxes clone next to the plugin where pnpm install has run.
    • Import from the package root (import { Button } from "erxes-ui"). The remote's hostShared list in ui/rspack.config.ts mirrors Core UI's shared singletons (react, react-dom, react-router, react-router-dom, @apollo/client, jotai, react-i18next, erxes-ui, ui-modules) with import: false: the remote never bundles them. A deep import such as erxes-ui/components/button would bypass the singleton and bundle a second copy.
    • When the libraries are published to npm, replace the git specifiers with version ranges; nothing else changes.
  4. Enable and run

    In the erxes checkout, add the plugin name to .env and restart pnpm dev:core-api (or pnpm dev:apis) and pnpm dev:uis:

    ENABLED_PLUGINS=sales,inventory
    

    In the plugin repository, copy api/.env.example to api/.env. The generated defaults point at local Redis and announce the UI dev server:

    REDIS_HOST=localhost
    REDIS_PORT=6379
    MONGO_URL=mongodb://localhost:27017/erxes   # platform stack
    UI_ENTRY_URL=http://localhost:3099/remoteEntry.js
    

    Then start both parts:

    pnpm dev:api    # registers erxes-service-inventory and the config key in Redis
    pnpm dev:ui     # serves http://localhost:3099/remoteEntry.js
    

    Core API returns the registered uiEntry from GET /get-frontend-plugins, and Core UI in development registers remotes from that response alongside the monorepo's own. Open http://localhost:3001: the host loads inventory_ui/config, adds the navigation entry, and routes /inventory/* to the remote. The remote's own port is never the app entry point.

  5. Check it works

    pnpm check && pnpm lint && pnpm build
    

    build emits api/dist and ui/dist/remoteEntry.js. Then check end to end:

    • Redis has erxes-service-inventory and erxesservice:config:inventory, and the gateway composes the plugin's subgraph.
    • The host renders the plugin's navigation and page without a console error about a missing remote.
    • Queries and mutations work for a permitted user and update the UI immediately.
  6. Deploy

    The API and the UI deploy separately:

    • API: docker build --build-arg UI_ENTRY_URL=<remoteEntry.js URL> -t <image> . builds an API-only image that registers itself on start. The gateway reaches it at LOAD_BALANCER_ADDRESS, or http://plugin-inventory-api:<port> when that is unset, and the gateway's own ENABLED_PLUGINS must list the plugin.
    • UI: upload ui/dist to any static host and point UI_ENTRY_URL at its remoteEntry.js. Cache hashed assets long and remoteEntry.js with Cache-Control: no-store. Without UI_ENTRY_URL, erxes looks for the remote at plugins.erxes.io/<release>/inventory_ui/remoteEntry.js, which only exists for plugins erxes publishes.

    Production and development use the same discovery path, so a plugin that works locally against pnpm dev:uis works against a deployed Core UI once its API and remote are reachable. See Deployment for the erxes side.

Names that must stay consistent

All of these come from erxes.json; change them only there, and only together.

NameExampleUsed by
nameinventoryRedis keys, ENABLED_PLUGINS, the docker host plugin-inventory-api, the CDN folder inventory_ui
ui.nameinventoryCONFIG.name, the page expose ./inventory, permission checks
ui.remoteinventory_uithe Module Federation container; Core API derives it from name
ui.pathinventorythe route /inventory/* in Core UI
GraphQL prefixinventory / Inventoryoperation, field, and type names, unique across the supergraph

Core UI expects the remote to expose ./config (the IUIConfig export named CONFIG) and ./<ui.name>; the generated ui/rspack.config.ts reads both from erxes.json. The plugin's Tailwind classes use the inventory: prefix and the host's design tokens so styles do not collide with other remotes.

Troubleshooting

  • pnpm install finishes but erxes-ui types resolve to source files with errors: the library's prepare did not run. Confirm the three packages are listed under onlyBuiltDependencies in pnpm-workspace.yaml; with file: specifiers, run pnpm install in the erxes clone first so its dist/ exists.
  • Core UI never shows the plugin: check that the name is in the erxes ENABLED_PLUGINS, that UI_ENTRY_URL is reachable from the browser, and that Core API restarted after the .env change. Core UI waits only briefly for /get-frontend-plugins in development, so a slow Core API means the remote is skipped until the next reload.
  • The page loads but hooks or routing fail: a shared library got bundled into the remote. Check that the hostShared list still has import: false and that imports use the package root, not deep paths.
  • The API starts but the gateway never composes it: check Redis reachability, the erxes-service-inventory key, and that the plugin's GraphQL types carry the prefix (a bare Query field name used by another plugin fails composition).
  • bun with the platform stack stops with an error: that stack runs on Node.js only; use express with Bun or switch to Node.js.

Next steps

Was this helpful?