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, and pnpm 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

  1. Run the generator

    From the repository root, run the interactive generator:

    pnpm create-plugin
    

    Or pass both names for a repeatable run:

    pnpm create-plugin --plugin-name=inventory --module-name=items
    # short flags: -p inventory -m items
    

    Names 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 (myPlugin becomes my-plugin). It creates two Nx projects:

    • backend/plugins/inventory_api (Nx project inventory_api)
    • frontend/plugins/inventory_ui (Nx project inventory_ui)
  2. 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.tsx
    

    Both default ports collide with existing plugins: API 33010 belongs to an Enterprise Edition plugin and UI 3005 is sales_ui's port. Pick unused values. Ports already in use:

    • API: core 3300; plugins 3303–3305, 3307–3313, 33010; gateway 4000.
    • UI: 3002–3011; core-ui host 3001.

    process.env.PORT overrides the API port at runtime, so keep the registered port and any hard-coded references consistent.

  3. Enable the plugin locally

    Add the base name (no _api or _ui suffix) to .env:

    ENABLED_PLUGINS=sales,inventory
    

    Comma-separated, no spaces. ENABLED_PLUGINS_ONLY_API exists for API-only plugins and takes the same base names. Restart the dev processes after changing the list.

  4. 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:uis
    

    Open 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_api or pnpm nx serve inventory_ui runs one project, but core-api and the gateway must still be running.

  5. Implement the first feature inside the plugin boundary

    Stay inside the two plugin directories. Use erxes-api-shared, erxes-ui, and ui-modules through 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.

  6. Check it works

    pnpm nx lint inventory_api && pnpm nx build inventory_api
    pnpm nx lint inventory_ui && pnpm nx build inventory_ui
    

    List 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.tsx path maps to a real route and each expose maps to a real named export; the host loads ./<name> and ./<name>Settings by 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, and jest.config.ts are 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.md whenever 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

  • EADDRINUSE on start: the generated 33010/3005 ports 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.tsx path with your routes, check expose names and named exports, and inspect the remote's network request.
  • erxes-api-shared import fails: run pnpm nx build erxes-api-shared, then restart the API.

Next steps

Was this helpful?