Frontend & Plugin Loading
How the Core UI host discovers and loads plugin micro-frontends at runtime.
For generating a new remote, see Create a Plugin and Frontend Plugins. For the backend half of plugin registration, see Backend & API Gateway.
Host and remotes
erxes uses Module Federation (@module-federation/enhanced on Rspack) with one host and one remote per frontend plugin.
- Host:
frontend/core-ui.src/main.tsdynamically importssrc/bootstrap.tsx, which initializes Sentry and then the federation runtime before renderingApp. The dev server listens on3001. - Remotes:
frontend/plugins/<name>_ui, each an Nx project with its ownmodule-federation.config.ts,rspack.config.ts, and dev port. - Shared singletons: every
module-federation.config.tsshares the samecoreLibrariesset (react,react-dom,react-router,react-router-dom,erxes-ui,@apollo/client,jotai,ui-modules,react-i18next,radix-ui), so all remotes run against the host's copies. Anything not in the set is bundled per remote.
Plugin dev ports (Community Edition): content 3003, frontline 3004, sales 3005, operation 3006.
Development versus production
In development, the host's module-federation.config.ts builds its remotes list from ENABLED_PLUGINS (sales becomes sales_ui), and scripts/start-ui-dev.js passes them to Nx as --devRemotes. bootstrap.tsx then renders <App /> directly. Run:
pnpm dev:uis
with ENABLED_PLUGINS=sales in .env to serve core-ui and sales_ui together.
In production, bootstrap.tsx instead fetches ${REACT_APP_API_URL}/get-frontend-plugins and passes the response to init({ name: 'core', remotes }) before rendering. Core API serves that list in organization/routes.ts:
[{ "name": "sales_ui", "entry": "https://plugins.erxes.io/latest/sales_ui/remoteEntry.js" }]
- The remote name is
<plugin>_uiwith dashes converted to underscores (federation container names cannot contain dashes). - The entry is
https://plugins.erxes.io/<version>/<plugin>_ui/remoteEntry.js.<version>is thereleaseVersionthe plugin's API registered in Redis (RELEASE_VERSIONwhen it starts with3., elselatest). UnderVERSION=saas, the list is additionally filtered by the organization's purchased charges and always includesagent_ui. - Plugin UIs are not Docker images: the
ci-ui-*workflows build each remote andaws s3 syncit to an R2 bucket that frontsplugins.erxes.io, underlatest/or the release tag. A UI release therefore takes effect through the CDN, not a container redeploy.
If the fetch fails, the host renders ClientConfigError instead of a broken app. A plugin that registers in Redis but whose remoteEntry.js is unreachable still appears in /get-frontend-plugins; check the browser console for loadRemote errors on <name>_ui/config.
How config.tsx is consumed
After init, PluginConfigsProvidersEffect (PluginConfigsProvidersEffect.tsx) calls loadRemote('<remote>/config') for every registered remote and stores each module's CONFIG export in the pluginsConfigState Jotai atom. The CONFIG object follows IUIConfig from erxes-ui (UIConfig.ts):
| Field | Consumed by |
|---|---|
name, path | Route registration and permission guard |
navigationGroup | Main navigation rail: icon, defaultPath, content, subGroup |
settingsNavigation | Settings sidebar entries |
modules[] | Route entries (name, path, hasAutomation, widget flags) |
widgets | relationWidgets, customerDetailWidgets, formWidgets, propertyInputs, activityRows |
searchProviders | Global search providers for command search |
i18n / i18nNamespace | Extra translation namespaces loaded on demand |
Routing works by convention:
usePluginsRouter(usePluginsRouter.tsx) mounts/<config.path>/*and lazy-loads<pluginName>_ui/<module.name>throughRenderPluginsComponent, guarded byPermissionRouteGuard.- Settings routes load
<pluginName>_ui/<plugin.name>Settings. resolveRemoteComponenttriesdefault, the PascalCase module name,<Name>Component,<Name>RemoteEntry,<Name>RemoteEntries,<Name>Widget, and finally any component-shaped export. Exposed modules must use named exports, so keep the convention rather than relying on the fallbacks.
What a remote exposes
Each remote declares exposes in its own module-federation.config.ts. Sales (sales_ui) is representative:
exposes: {
'./config': './src/config.tsx',
'./sales': './src/modules/Main.tsx',
'./dealsSettings': './src/pages/SalesSettingsIndexPage.tsx',
'./salesSettings': './src/pages/SalesSettingsIndexPage.tsx',
'./Widgets': './src/widgets/Widgets.tsx',
'./notificationWidget': './src/widgets/notifications/NotificationsWidgets.tsx',
'./relationWidget': './src/widgets/relation/RelationWidgets.tsx',
'./pos': './src/modules/pos/Main.tsx',
'./posSettings': './src/modules/pos/pos/Main.tsx',
'./automationsWidget': './src/widgets/automations/components/AutomationRemoteEntry.tsx',
},
./config is mandatory; it is the only module the host loads eagerly. Every other expose must match a CONFIG.modules path, a widget declaration, or a host reference. Keep expose names, config.tsx paths, and real routes aligned; see Frontend Plugins.