Create a Plugin
Create paired backend and frontend projects in the erxes monorepo, enable them locally, and validate the first working feature. To develop a plugin in its own repository instead, see Standalone Plugins.
Prerequisites
- Complete Local Setup: pnpm ≥ 8, MongoDB, Redis,
pnpm install, andpnpm nx build erxes-api-shared. - A unique plugin name, its first business module, and unused API and UI ports (step 2 lists the ports already taken).
- Decide whether the plugin needs the backend, the frontend, or both. The generator always creates both; for a one-sided plugin you remove the unused project right away.
Generate and connect a plugin
Run the generator
From the repository root, run the interactive generator:
pnpm create-pluginOr pass both names for a repeatable run:
pnpm create-plugin --plugin-name=inventory --module-name=items # short flags: -p inventory -m itemsNames must match
^[a-zA-Z][a-zA-Z0-9]*$: start with a letter, letters and digits only. The generator converts camelCase to kebab-case for directory names (myPluginbecomesmy-plugin). It creates two Nx projects:backend/plugins/inventory_api(Nx projectinventory_api)frontend/plugins/inventory_ui(Nx projectinventory_ui)
Review the generated files and ports
The generator (create-plugin.js, create-backend-plugin.js) writes these files:
backend/plugins/inventory_api/ package.json "dev": "nodemon src/main.ts"; dep: erxes-api-shared project.json targets: build, build:packageJson, start, serve, docker-build tsconfig.json, tsconfig.build.json .gitignore src/main.ts startPlugin({ name: 'inventory', port: 33010, ... }) src/connectionResolvers.ts createGenerateModels<IModels>(loadClasses) src/apollo/typeDefs.ts apolloCommonTypes + extend type Query/Mutation src/apollo/schema/schema.ts src/apollo/resolvers/{index,mutations,queries,resolvers}.ts src/trpc/init-trpc.ts appRouter with a hello procedure src/trpc/trpcClients.ts coreTRPCClient via getPlugin('core') src/modules/items/ @types, db/{definitions,models}, graphql/{schemas,resolvers/{queries,mutations,customResolvers}} frontend/plugins/inventory_ui/ project.json serve port 3005; build/serve/serve-static targets module-federation.config.ts rspack.config.ts, rspack.config.prod.ts tsconfig.json, tsconfig.app.json, tsconfig.spec.json jest.config.ts, eslint.config.js src/index.html, src/main.ts, src/bootstrap.tsx src/config.tsx IUIConfig with navigationGroup + one module src/assets/{example-icon.svg, example-image.svg, README.md} src/modules/{InventoryMain,InventoryNavigation,InventorySettingsNavigation,InventorySettings}.tsx src/pages/items/IndexPage.tsx src/widgets/Widgets.tsxBoth default ports collide with existing plugins: API
33010belongs to an Enterprise Edition plugin and UI3005issales_ui's port. Pick unused values. Ports already in use:- API: core
3300; plugins3303–3305,3307–3313,33010; gateway4000. - UI:
3002–3011; core-ui host3001.
process.env.PORToverrides the API port at runtime, so keep the registered port and any hard-coded references consistent.- API: core
Enable the plugin locally
Add the base name (no
_apior_uisuffix) to.env:ENABLED_PLUGINS=sales,inventoryComma-separated, no spaces.
ENABLED_PLUGINS_ONLY_APIexists for API-only plugins and takes the same base names. Restart the dev processes after changing the list.Start the dev processes
# Terminal 1: core-api, gateway, and every *_api in ENABLED_PLUGINS pnpm dev:apis # Terminal 2: core-ui host plus every *_ui remote pnpm dev:uisOpen
http://localhost:3001: the core-ui host loads your remote, so the remote's own port is not the app entry point. For focused work,pnpm nx serve inventory_apiorpnpm nx serve inventory_uiruns one project, but core-api and the gateway must still be running.Implement the first feature inside the plugin boundary
Stay inside the two plugin directories. Use
erxes-api-shared,erxes-ui, andui-modulesthrough their public exports only: no cross-plugin imports, no edits to core or shared libraries. See Backend Plugins and Frontend Plugins for the service and host APIs, and Plugin Metadata & Extensions for permissions and hooks.Check it works
pnpm nx lint inventory_api && pnpm nx build inventory_api pnpm nx lint inventory_ui && pnpm nx build inventory_uiList a project's targets with
pnpm nx show project inventory_api. Then check the plugin end to end:- The API registers with the gateway and composes into the supergraph.
- The host renders your navigation and route.
- A permitted user can create, read, update, and delete records with immediate UI feedback.
The scaffold is not a deliverable
Generated code is a starting point, not a working plugin. The generated IndexPage, for example, includes placeholder text ("Add your content here") and a More button with no handler. Before treating the plugin as usable:
- Replace sample pages, widgets, models, schemas, resolvers, and placeholder text with complete behavior, or remove them.
- Replace the generated ports (
33010,3005) with unused plugin-specific ports. - Make plugin name, module name, routes, navigation paths, and Module Federation exposes consistent.
- Make sure each
config.tsxpath maps to a real route and each expose maps to a real named export; the host loads./<name>and./<name>Settingsby convention. - Remove unused generated files and imports.
- Fix generated types, validation, loading states, empty states, error states, and mutation feedback.
- Use named exports; default exports are allowed only where a tool requires them (the generated
module-federation.config.ts,rspack.config.ts, andjest.config.tsare such cases). - Add only the plugin's own dependencies; do not add a dependency a platform package already covers.
- Create or update each plugin's
AGENTS.mdwhenever source, config, schema, routes, or user-visible behavior changes; the repo requires one at the root of each plugin project (inventory_api,inventory_ui). Its sections, in order: Identity, Scope, Current Capabilities, Architecture, Contracts, Data and State, Local Invariants, Validation, Recent Changes.
Troubleshooting
EADDRINUSEon start: the generated33010/3005ports are still set and collide with existing plugins.- Nx cannot find the project: project names are
inventory_api/inventory_ui, not the directory path. - Plugin API never joins the gateway: check the name in
ENABLED_PLUGINS, startup errors, Redis reachability, and the registered address (erxes-service-<name>key). - Navigation renders but the page fails: compare the
config.tsxpath with your routes, check expose names and named exports, and inspect the remote's network request. erxes-api-sharedimport fails: runpnpm nx build erxes-api-shared, then restart the API.