# Frontend & Plugin Loading

Core UI host and Module Federation remotes


How the Core UI host discovers and loads plugin micro-frontends at runtime.

For generating a new remote, see [Create a Plugin](https://erxes.io/docs/create-plugin) and [Frontend Plugins](https://erxes.io/docs/plugin-frontend). For the backend half of plugin registration, see [Backend & API Gateway](https://erxes.io/docs/architecture-backend-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.ts` dynamically imports `src/bootstrap.tsx`, which initializes Sentry and then the federation runtime before rendering `App`. The dev server listens on `3001`.
- **Remotes:** `frontend/plugins/<name>_ui`, each an Nx project with its own `module-federation.config.ts`, `rspack.config.ts`, and dev port.
- **Shared singletons:** every `module-federation.config.ts` shares the same `coreLibraries` set (`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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/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:

```bash
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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/organization/routes.ts):

```json
[{ "name": "sales_ui", "entry": "https://plugins.erxes.io/latest/sales_ui/remoteEntry.js" }]
```

- The remote name is `<plugin>_ui` with dashes converted to underscores (federation container names cannot contain dashes).
- The entry is `https://plugins.erxes.io/<version>/<plugin>_ui/remoteEntry.js`. `<version>` is the `releaseVersion` the plugin's API registered in Redis (`RELEASE_VERSION` when it starts with `3.`, else `latest`). Under `VERSION=saas`, the list is additionally filtered by the organization's purchased charges and always includes `agent_ui`.
- Plugin UIs are not Docker images: the `ci-ui-*` workflows build each remote and `aws s3 sync` it to an R2 bucket that fronts `plugins.erxes.io`, under `latest/` 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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/modules/plugins/providers/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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/libs/erxes-ui/src/types/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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/modules/app/hooks/usePluginsRouter.tsx)) mounts `/<config.path>/*` and lazy-loads `<pluginName>_ui/<module.name>` through `RenderPluginsComponent`, guarded by `PermissionRouteGuard`.
- Settings routes load `<pluginName>_ui/<plugin.name>Settings`.
- `resolveRemoteComponent` tries `default`, 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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/sales_ui/module-federation.config.ts)) is representative:

```ts
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](https://erxes.io/docs/plugin-frontend).

