# Introduction What erxes is, how the platform fits together, and where to start erxes is a self-hosted, source-available Experience Operating System (XOS) that combines customer data, marketing, sales, operations, and support in one platform. It is developed as a single Nx-powered pnpm monorepo that you extend through plugins. ## Choose your starting point - [Local Setup](https://erxes.io/docs/local-setup): Run the product from source with MongoDB and Redis. - [Deployment](https://erxes.io/docs/self-hosting): Deploy erxes on your own infrastructure. - [Create a Plugin](https://erxes.io/docs/create-plugin): Add a business capability with backend and frontend projects. - [Messenger Widget](https://erxes.io/docs/erxes-messenger): Embed chat and forms on a website. - [CMS + Next.js](https://erxes.io/docs/content-plug-cms-next-developers-introduction): Build a website backed by Content plugin data. - [Contributing](https://erxes.io/docs/contribute): Propose a fix or improve the documentation. ## What can you do with erxes? The core platform provides shared capabilities such as contacts, products, properties, segments, documents, automations, and permissions. Plugins add business-specific functionality. The Community Edition includes backend plugins for sales, operations, frontline customer service, content management, and POS (`posclient`); further plugins are part of the Enterprise Edition, licensed separately. As a developer, you can: - Extend the platform with backend services and frontend plugins. - Integrate applications through GraphQL and published service interfaces. - Build customer-facing experiences with the client portal, help center, and widgets. - Self-host an installation and manage its infrastructure and data, subject to the repository's license terms. ## How the parts work together The Core UI is the browser entry point. It loads plugin UI modules through Module Federation and sends GraphQL requests to the gateway. Plugin UIs extend navigation and feature screens while reusing the platform's public UI libraries. On the backend, Core API and plugin APIs register their addresses and configuration in Redis. The gateway composes their GraphQL schemas for Apollo Router and exposes a federated `/graphql` endpoint. Service-to-service calls use tRPC procedures defined by the module that provides them. Backend model creation uses the request subdomain to select the tenant's data. Redis also supports queues and subscriptions. Background services such as automations and logs need their own running processes when those features are used. An enabled plugin name selects projects or services for discovery; it does not provision production containers or external integrations. Standalone apps such as widgets and the client portal have separate builds and deployment configuration. In production, Core API returns plugin asset URLs hosted at `plugins.erxes.io`, so not all browser assets come from your own host; see [Deployment](https://erxes.io/docs/self-hosting). ## Technology stack The toolchain versions below are declared in the root manifest and lockfile. | Area | Version | | --- | --- | | Package manager | pnpm `9.12.3` (required, `packageManager` field) | | Build system | Nx `20.0.8` with caching and task orchestration | | Runtime | Node.js 22 in CI | | Language | TypeScript `5.7.3` | ### Backend The gateway (port 4000) runs Apollo Router in front of Core API (port 3300) and one Express service per enabled plugin API. ```text API Gateway (port 4000) Apollo Router + service discovery | ┌─────────────┼─────────────┐ ▼ ▼ ▼ Core API Plugin APIs Background (3300) (3303+) services (3301-3302) └─────────────┼─────────────┘ ▼ MongoDB + Redis/BullMQ ``` - **Framework:** Express 4 with Apollo Server v4 and Apollo Federation (`@apollo/subgraph`) - **Service calls:** tRPC v11 for type-safe service-to-service procedures - **Database:** MongoDB via Mongoose `8.13.2` - **Cache and queues:** Redis (`ioredis` 5) and BullMQ 5 - **Real-time:** GraphQL subscriptions over `graphql-redis-subscriptions` - **Authentication:** JWT (`jsonwebtoken`) and WorkOS SSO ### Frontend The Core UI host (port 3001) loads each enabled `*_ui` remote through Module Federation. Plugin remotes use development ports 3002-3011. - **Framework:** React `18.3.1` with React Router v7 - **Bundler:** Rspack `1.0.5` with `@module-federation/enhanced` `0.6.6` - **Styling:** Tailwind CSS v4 + PostCSS - **UI:** `erxes-ui`, the Radix-based design system, and `ui-modules` for reusable business UI - **State:** Jotai + Apollo Client; React Hook Form + Zod; `react-i18next` ### Standalone apps Apps under `apps/` carry their own dependencies; the React 18 version above applies to the main frontend only. | App | Stack | Dev port | | --- | --- | --- | | `client-portal-template` | Next.js `16.0.3`, React 19 | 3800 | | `help-center` | Next.js `16.3.2`, React 19 | 3900 | | `posclient-front` | Next.js 14, React 18, PWA | 7002 | | `frontline-widgets` | Rspack bundle (chat, forms) | n/a | ## Repository structure The main development areas; [Repository Structure](https://erxes.io/docs/repository-structure) has the full map with per-plugin ports. ```text erxes/ ├── backend/ # Backend microservices │ ├── gateway/ # API gateway + Apollo Router (4000) │ ├── core-api/ # Core business logic (3300) │ ├── erxes-api-shared/ # Shared library for all services │ ├── plugins/ # Plugin APIs (*_api, ports 3303+) │ ├── services/ # Background services (automations, logs) │ └── saas-migrations/ # Standalone migration scripts ├── frontend/ │ ├── core-ui/ # Module Federation host (3001) │ ├── libs/ # erxes-ui + ui-modules │ └── plugins/ # Plugin remotes (*_ui, ports 3002-3011) ├── apps/ # Standalone apps (portal, help center, POS, widgets) ├── scripts/ # create-plugin, start-api-dev, start-ui-dev └── cloudflare/mail-worker/ # Inbound email routing worker ``` ## Source code and community The product source is on GitHub. Read the repository's contribution guide before changing core or plugin code. - [erxes](https://github.com/erxes/erxes): the product codebase - [Contributing guide](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CONTRIBUTING.md): how to contribute Join the [erxes Discord](https://discord.gg/erxes) to ask development questions, discuss integrations, and share your work with the community. ## Licensing and deployment erxes is source-available. The repository's [LICENSE.md](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/LICENSE.md) contains AGPLv3 provisions, references separate Enterprise Edition terms, and states that erxes may not be hosted as a SaaS product that competes with erxes Inc. Third-party components retain their respective licenses. Self-hosting, modifying, and distributing erxes remain subject to those terms; source availability does not mean every plugin or commercial deployment is unrestricted. For hosted offerings, Enterprise options, and pricing, see [erxes](https://erxes.io). When self-hosting, your team is responsible for deployment, upgrades, backups, and ongoing infrastructure maintenance. --- # Local Setup Run the backend, gateway, and Core UI from source with MongoDB and Redis Run the erxes backend and main frontend from source, with MongoDB and Redis running locally. Run commands from the repository root unless a step says otherwise. The commands below use a Unix-compatible shell on macOS or Linux; on Windows, use WSL2. ## Prerequisites - [Git](https://git-scm.com/downloads) to clone the repository. - [Node.js 22](https://nodejs.org/en/download/), matching the backend CI runtime. A version manager such as [nvm](https://github.com/nvm-sh/nvm) is optional. - [pnpm 9.12.3](https://pnpm.io/installation), the version declared in the root `package.json` `packageManager` field. - [Docker with Compose](https://docs.docker.com/compose/install/) for the database example below, or locally installed MongoDB and Redis services. - `curl` and internet access for the gateway's first-run Apollo Router download. Nx is a repository dependency. Use `pnpm nx` after installing dependencies; a global Nx installation is unnecessary. If you use nvm and Corepack, select the toolchain with: ```bash nvm install 22 nvm use 22 corepack enable corepack prepare pnpm@9.12.3 --activate ``` Check the active versions: ```bash node --version pnpm --version ``` ## Clone the repository ```bash git clone https://github.com/erxes/erxes.git cd erxes ``` To work from a release instead of the development branch, check out a tag (`git tag -l` lists them). A tag checkout puts Git in detached-HEAD mode; create a branch before making changes, and see the [contribution workflow](https://erxes.io/docs/contribute-codebase) when targeting an upstream contribution. If you already have a checkout, open a terminal at its root instead. The root contains `package.json`, `pnpm-workspace.yaml`, `backend/`, and `frontend/`. ## Start MongoDB and Redis The following development example uses MongoDB 7 as a single-node replica set and Redis 7. A replica set supports the multi-document transactions plugins run through `startSession()` calls, so use one even for core-only work. The Compose file is a development example, not a product installer; the databases run in Docker while erxes runs on your host machine. ### 1. Create the Compose file Create `docker-compose.local.yml` in the repository root: ```yaml services: mongo: image: mongo:7 command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] ports: - "127.0.0.1:27017:27017" volumes: - erxes-mongo:/data/db healthcheck: test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand({ ping: 1 })"] interval: 5s timeout: 5s retries: 20 redis: image: redis:7 ports: - "127.0.0.1:6379:6379" volumes: - erxes-redis:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 20 volumes: erxes-mongo: erxes-redis: ``` ### 2. Start the containers ```bash docker compose -f docker-compose.local.yml up -d --wait ``` Initialize the replica set once, when using a new MongoDB volume: ```bash docker compose -f docker-compose.local.yml exec mongo mongosh --quiet --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})' ``` ### 3. Check database health ```bash docker compose -f docker-compose.local.yml exec mongo mongosh --quiet --eval 'db.hello().isWritablePrimary' docker compose -f docker-compose.local.yml exec redis redis-cli ping ``` The expected responses are `true` and `PONG`. Election may take a few seconds; repeat the MongoDB check if it returns `false`. On later starts, reuse the volumes and skip `rs.initiate()`. ### Using existing local databases You can instead install [MongoDB](https://www.mongodb.com/docs/manual/installation/) and [Redis](https://redis.io/docs/latest/install/) directly. Start both services and configure MongoDB as a [replica set](https://www.mongodb.com/docs/manual/tutorial/deploy-replica-set/) for the setup above. Adjust the connection settings below to match your local ports, replica-set name, and authentication settings. ## Configure the environment Copy the repository's [sample file](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.env.sample) if you do not already have a local `.env`: ```bash cp .env.sample .env ``` Most sample entries are commented out, and several variables below are not in the sample. Open `.env` in your editor and add or uncomment these values for the Docker setup above: ```dotenv NODE_ENV=development VERSION=os MONGO_URL=mongodb://127.0.0.1:27017/erxes?replicaSet=rs0&directConnection=true REDIS_HOST=127.0.0.1 REDIS_PORT=6379 DOMAIN=http://localhost:3001 REACT_APP_API_URL=http://localhost:4000 GATEWAY_URL=http://localhost:4000 ENABLED_PLUGINS=sales ``` - `REDIS_HOST` and `REDIS_PORT` are read by the shared Redis client. `REDIS_URL` does not configure that client. Set `REDIS_PASSWORD` too if your Redis server requires authentication. - `DOMAIN` is the frontend origin; `REACT_APP_API_URL` points the frontend to the gateway. Keep these origins consistent with the URLs you open in the browser. - `GATEWAY_URL` supplies the directly reachable gateway address for generated email links; unset, Core API falls back to `${DOMAIN}/gateway`. - `ENABLED_PLUGINS=sales` starts the existing `sales_api` and `sales_ui` projects. Use base plugin names, not the `_api` or `_ui` suffixes. The startup scripts append the suffix themselves, so `ENABLED_PLUGINS=sales_api` looks for a nonexistent `sales_api_api` project. - Multiple plugins use a comma-separated list without spaces, for example `ENABLED_PLUGINS=sales,operation`. Only list plugins present in your checkout. - For core-only development, omit `ENABLED_PLUGINS` or leave it empty. The startup scripts handle both cases. Do not set it to the literal text `null` or a placeholder name. Optional background services can be enabled with: ```dotenv ENABLED_SERVICES=automations,logs ``` The API startup script maps these names to `automations-service` and `logs-service`. Enable them when working with automation execution and persisted logs; the logs service also persists the automatic undo journal. `ENABLED_PLUGINS_ONLY_API` can enable additional backend-only plugins without adding frontend remotes. Set a private `JWT_TOKEN_SECRET` for local authentication instead of relying on the source-code fallback (`'SECRET'`). Generate a value with `openssl rand -hex 32` and add it to your local `.env`. Keep that value consistent across backend services and out of source control. Restart the development processes after changing plugin or environment settings. ## Install dependencies and build the shared API library ```bash pnpm install pnpm nx build erxes-api-shared ``` Backend services use the compiled entry points of `erxes-api-shared`. Build it before starting them, and rebuild it after changing that library's source. The root pnpm workspace includes `backend/**` and `frontend/**`. Standalone applications under `apps/`, such as the client portal and help center, have separate setup requirements and are not started by the commands below. ## Start erxes Use two terminals, both at the repository root. ### 1. Start the backend ```bash pnpm dev:apis ``` The script ([start-api-dev.js](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/start-api-dev.js)) starts Core API, the gateway, enabled plugin APIs, and any enabled background services. With the example configuration, it includes `sales_api` on port `3305`. On first startup, the gateway downloads Apollo Router `v1.59.2` into `backend/gateway/src/apollo-router/temp/` and composes the federated GraphQL schema from the registered services. Wait for the APIs and router to finish starting before opening the UI. The router listens on internal port `50000` by default (`APOLLO_ROUTER_PORT` overrides it); clients connect through the gateway on `4000`. ### 2. Start the frontend ```bash pnpm dev:uis ``` The script ([start-ui-dev.js](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/start-ui-dev.js)) starts the Core UI host on port `3001` and the frontend remotes listed in `ENABLED_PLUGINS`. With `sales` enabled, its remote runs on port `3005`. Use the Core UI host as the application entry point. ## Check it works 1. Check the backend health endpoints: ```bash curl --fail http://localhost:3300/health curl --fail http://localhost:4000/health ``` Both should return `ok`. A gateway health response confirms the HTTP server is running; it does not by itself confirm that GraphQL composition has finished. 2. Check the federated endpoint separately: ```bash curl --fail http://localhost:4000/graphql \ -H 'Content-Type: application/json' \ --data '{"query":"query LocalSetupHealth { __typename }"}' ``` Once the router is ready, the response should contain `data` with `__typename` set to `Query`. HTTP 200 with a GraphQL `errors` array is not a successful API check. 3. Open [http://localhost:3001](http://localhost:3001). On a fresh installation without an owner, the frontend calls `/initial-setup` on the gateway, sees `hasOwner: false`, and directs you to `/create-owner`. Complete that form to create your initial account. If an owner already exists, sign in with the account for that local database. ### Local service URLs | Service | URL | Purpose | | --- | --- | --- | | Core UI | [localhost:3001](http://localhost:3001) | Main application | | Gateway health | [localhost:4000/health](http://localhost:4000/health) | Gateway HTTP health check | | GraphQL | [localhost:4000/graphql](http://localhost:4000/graphql) | Federated API endpoint | | Core API health | [localhost:3300/health](http://localhost:3300/health) | Core API health check | | Sales API | `http://localhost:3305` | Enabled by the example plugin configuration | | Sales UI remote | `http://localhost:3005` | Loaded by the Core UI host | ## Troubleshooting ### MongoDB or Redis connection refused Check that the containers are running and healthy: ```bash docker compose -f docker-compose.local.yml ps docker compose -f docker-compose.local.yml logs mongo redis ``` Match the published ports with `MONGO_URL`, `REDIS_HOST`, and `REDIS_PORT`. For this host-based setup, use `127.0.0.1`, not Docker service names such as `mongo` or `redis`. ### MongoDB reports no primary or replica-set errors Initialize the replica set on a new volume, then confirm `db.hello().isWritablePrimary` returns `true`. The URI's `replicaSet` value must match the configured set name (`rs0` in this guide). An `AlreadyInitialized` response means the volume already has a replica-set configuration; inspect it with `rs.conf()` instead of initializing it again. If a plugin fails with `Transaction numbers are only allowed on a replica set member or mongos`, your `mongod` is running standalone; convert it to a replica set. ### Apollo Router download fails on first startup The gateway fetches the router binary with `curl -sSL https://router.apollo.dev/download/nix/v1.59.2 | sh` into `backend/gateway/src/apollo-router/temp/` whenever that file is missing and `NODE_ENV` is not `production`. A network restriction or interrupted download leaves the gateway stuck at startup. Check outbound access to `router.apollo.dev`, delete a partial `temp/` directory, and restart `pnpm dev:apis`. You can also run the same `curl` command inside `backend/gateway/src/apollo-router/temp/` manually, as the gateway's error message suggests. ### A plugin project or remote cannot be found `ENABLED_PLUGINS` takes base names: `sales` maps to `sales_api` and `sales_ui`. Writing `sales_api` produces `sales_api_api`, which does not exist. Check spelling, remove spaces and trailing commas, and confirm both matching projects exist. `posclient` has a backend plugin but no `posclient_ui`, so it belongs in `ENABLED_PLUGINS_ONLY_API`, not `ENABLED_PLUGINS`. Restart both development terminals after changing the list. ### Cannot resolve erxes-api-shared Run `pnpm nx build erxes-api-shared` from the repository root, then restart the affected API. Repeat the build after editing the shared library. ### GraphQL is unavailable but gateway health is OK Inspect the backend terminal for service registration, schema composition, or Apollo Router errors. Confirm the selected APIs are running and that port `50000` is available. The first run requires access to the Apollo Router download service. ### A port is already in use Stop the other process using the reported port. The default setup needs ports `27017`, `6379`, `3300`, `4000`, `50000`, and `3001`, plus each enabled plugin's API and UI ports. Avoid running a second copy of Core API or the gateway alongside `pnpm dev:apis`. ### Node.js runs out of heap memory Start with fewer plugins. If your machine has enough available memory, add `NODE_OPTIONS=--max-old-space-size=4096` to `.env` and restart the development processes. This limit applies per Node.js process, so account for the number of services you run concurrently. ## Stop and restart Press `Ctrl+C` in each development terminal. Stop the database containers with: ```bash docker compose -f docker-compose.local.yml down ``` The named volumes retain your local data. To resume, start the containers with `up -d --wait`, then run `pnpm dev:apis` and `pnpm dev:uis` again. Reinitializing the replica set is unnecessary when reusing the same MongoDB volume. --- # Repository Structure Map backend, frontend, plugins, and apps in erxes/erxes Find any project in `erxes/erxes`: backend services, plugin projects, frontend remotes, shared libraries, standalone apps, and the config files that tie them together. For running the product from source, use [Local Setup](https://erxes.io/docs/local-setup). For the service runtime, see [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway). ## Top-level layout ```text erxes/ ├── backend/ │ ├── core-api/ Core business logic (port 3300) │ ├── gateway/ API gateway + Apollo Router (port 4000) │ ├── erxes-api-shared/ Shared library for all services │ ├── plugins/ Plugin microservices (*_api, ports 3303+) │ ├── services/ Background services (automations, logs) │ └── saas-migrations/ Standalone one-off migration scripts ├── frontend/ │ ├── core-ui/ Module Federation host (port 3001) │ ├── libs/ erxes-ui + ui-modules shared UI │ └── plugins/ Frontend remotes (*_ui, ports 3002-3011) ├── apps/ Standalone apps (not started by dev:apis/dev:uis) ├── cloudflare/mail-worker/ Cloudflare Worker for inbound email routing ├── docs/ Scattered product notes (not the docs site) ├── scripts/ Dev scripts and plugin generators └── .github/workflows/ CI image builds per service/app ``` The root also carries `package.json`, `pnpm-workspace.yaml`, `nx.json`, `tsconfig.base.json`, `tsconfig.json`, `migrations.json`, `core-libraries.ts`, `.env.sample`, `eslint.config.js`, `jest.config.ts`, `index.html`, `CONTRIBUTING.md`, and `LICENSE.md`. There is no root `docker-compose.yml`; each deployable project has its own `Dockerfile`. ## Backend | Path | Nx project | Dev port | Purpose | | --- | --- | --- | --- | | `backend/gateway` | `gateway` | 4000 (`PORT`) | Express gateway; proxies `/graphql` to the internal Apollo Router on `50000` (`APOLLO_ROUTER_PORT`) | | `backend/core-api` | `core-api` | 3300 (`PORT`) | Core business modules, GraphQL subgraph, tRPC router | | `backend/erxes-api-shared` | `erxes-api-shared` | n/a | Shared library; build it before serving APIs | | `backend/services/automations` | `automations-service` | 3302 (`PORT`) | Automation execution workers | | `backend/services/logs` | `logs-service` | 3301 (`PORT`) | Persisted logs and the undo journal | | `backend/saas-migrations` | n/a | n/a | Standalone migration scripts grouped by domain | `backend/core-api/src/` holds `apollo/` (schema and resolvers), `modules/` (business domains such as contacts, products, segments, automations, documents), `meta/`, `trpc` wiring in `init-trpc.ts`, `routes/`, `connectionResolvers.ts`, and `main.ts`. `backend/gateway/src/` holds `apollo-router/` (router download, supergraph compose, spawn/recovery), `proxy/` (service proxying, including `/pl:` routes), `middlewares/`, `mq/`, `subscription/`, and `locales/`. `backend/erxes-api-shared` is a multi-entry package built with preconstruct: `src/utils/` (service discovery, Redis, tRPC, `startPlugin`), `src/core-modules/` (reusable domain logic), `src/core-types/`, and `src/common-modules/` compile to `utils/`, `core-modules/`, `core-types/` entry points. ### Backend plugins Each `backend/plugins/_api` starts through `startPlugin({ name, port, ... })` in `src/main.ts` and registers with the gateway via Redis service discovery (see [start-plugin.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/start-plugin.ts) and [service-discovery.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/service-discovery.ts)). Ports below come from each plugin's `src/main.ts`. | Plugin project | Plugin name | Dev port | | --- | --- | --- | | `content_api` | `content` | 3303 | | `frontline_api` | `frontline` | 3304 | | `operation_api` | `operation` | 3307 | | `posclient_api` | `posclient` | 3312 | | `sales_api` | `sales` | 3305 | A backend plugin typically contains `src/main.ts`, `src/connectionResolvers.ts`, `src/apollo/` (typeDefs/resolvers), `src/modules/`, `src/trpc/` or `init-trpc.ts`, `src/meta/`, plus `project.json`, `Dockerfile`, and `package.json`. ## Frontend | Path | Nx project | Dev port | Purpose | | --- | --- | --- | --- | | `frontend/core-ui` | `core-ui` | 3001 | Module Federation host; browser entry point | | `frontend/libs/erxes-ui` | `erxes-ui` | n/a | Core UI primitives, hooks, state, config | | `frontend/libs/ui-modules` | `ui-modules` | n/a | Reusable business-domain UI modules | `frontend/core-ui/src/` holds `main.ts`, `bootstrap.tsx` (renders directly in development; fetches `/get-frontend-plugins` and initializes remotes in production), `modules/`, `pages/`, `plugins/`, `providers/`, and `i18n/`. `module-federation.config.ts` maps `ENABLED_PLUGINS` to `_ui` remotes and shares a fixed set of libraries (react, react-router, `erxes-ui`, `ui-modules`, jotai, `@apollo/client`, `react-i18next`) as singletons. ### Frontend plugins Each `frontend/plugins/_ui` is a Module Federation remote served by Rspack. Ports below come from each plugin's `project.json` serve target. | Plugin project | Dev port | | --- | --- | | `content_ui` | 3003 | | `frontline_ui` | 3004 | | `operation_ui` | 3006 | | `sales_ui` | 3005 | A frontend plugin typically contains `src/config.tsx` (routes, navigation, remote registration), `src/modules/`, `src/pages/`, `src/widgets/`, `src/main.ts`, `src/bootstrap.tsx`, `module-federation.config.ts`, `rspack.config.ts`, and `project.json`. There is no `posclient_ui`; the POS frontend is the standalone app `apps/posclient-front`. ## Apps Standalone apps under `apps/` have their own dependencies and scripts. They are not part of `pnpm dev:apis` or `pnpm dev:uis`. | App | Stack | Dev command/port | | --- | --- | --- | | `apps/client-portal-template` | Next.js `16.0.3`, React 19 | `next dev --port 3800` | | `apps/help-center` | Next.js `16.3.2`, React 19 | `next dev --port 3900` | | `apps/posclient-front` | Next.js 14, React 18, PWA | `next dev -p 7002` | | `apps/frontline-widgets` | Rspack bundles (chat, forms) | `nx dev frontline-widgets` (serve + bundle watchers) | See [Client Portal](https://erxes.io/docs/app-client-portal), [Help Center](https://erxes.io/docs/app-help-center), [POS Client](https://erxes.io/docs/app-pos-client), and [Messenger Installation](https://erxes.io/docs/erxes-messenger). ## Scripts, workers, and misc directories - [scripts/start-api-dev.js](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/start-api-dev.js) maps `ENABLED_PLUGINS` and `ENABLED_PLUGINS_ONLY_API` to `*_api` projects and `ENABLED_SERVICES` to `*-service` projects, then runs `nx run-many -t serve -p core-api ... gateway`. - [scripts/start-ui-dev.js](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/start-ui-dev.js) maps `ENABLED_PLUGINS` to `*_ui` remotes via `nx serve core-ui --devRemotes`. - `scripts/create-plugin.js` scaffolds a paired `*_api` + `*_ui` plugin (`pnpm create-plugin`); `scripts/create-backend-plugin.js` scaffolds the backend half only. - `cloudflare/mail-worker` is a standalone Cloudflare Worker (own `package-lock.json`, `wrangler.toml`) that routes inbound email to the frontline plugin's `/mail/receive` webhook; the related `MAIL_*` variables are documented in `.env.sample`. - `docs/` contains isolated product notes, not the public documentation site. ## Root configuration files | File | Role | | --- | --- | | `package.json` | Root manifest: `dev:apis`, `dev:uis`, `dev:core-api`, `create-plugin` scripts; `packageManager: pnpm@9.12.3`; pnpm overrides | | `pnpm-workspace.yaml` | Workspace packages: `backend/**`, `frontend/**` (apps install separately) | | `nx.json` | Named inputs, Nx plugins (eslint, jest, webpack, storybook), target defaults, generator defaults (Rspack + Tailwind) | | `tsconfig.base.json` | Base compiler options and the shared path aliases below | | `tsconfig.json` | Solution-style file that references every project's tsconfig | | `migrations.json` | Nx migration entries recorded during past `nx migrate` runs | | `core-libraries.ts` | A shared-library name set (adds `radix-ui`); nothing imports it, so each `module-federation.config.ts` declares its own list | | `.env.sample` | Commented environment template | | `eslint.config.js`, `jest.config.ts`, `jest.preset.js` | Lint and test defaults | | `index.html` | Static messenger-widget test page | ## Path aliases `tsconfig.base.json` declares the shared aliases; each project adds local ones in its own `tsconfig.json`. | Alias | Resolves to | Where | | --- | --- | --- | | `erxes-ui` | `frontend/libs/erxes-ui/src/index.ts` | Root `tsconfig.base.json` | | `ui-modules` | `frontend/libs/ui-modules/src/index.ts` | Root `tsconfig.base.json` | | `erxes-api-shared`, `erxes-api-shared/*` | `backend/erxes-api-shared/src/index.ts`, `backend/erxes-api-shared/src/*` | Root `tsconfig.base.json` | | `~/*` | `/src/*` | Each backend service and frontend plugin tsconfig | | `@/*` | `/src/modules/*` | Each backend service and frontend plugin tsconfig | --- # Architecture How the gateway, services, frontend host, and shared libraries fit together How the gateway, backend services, frontend host, and shared libraries fit together at runtime. erxes is a single Nx/pnpm monorepo. On the backend, each service is an Express app that exposes an Apollo Federation subgraph; the gateway composes them into one supergraph behind a single public `/graphql` endpoint. On the frontend, one Module Federation host (`core-ui`) loads a remote per enabled plugin. MongoDB stores tenant data; Redis carries service discovery, session tokens, and every BullMQ queue. ## Key concepts - **One public endpoint.** The gateway on port `4000` ([main.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/main.ts)) is the only service clients call. It proxies `/graphql` to an Apollo Router it spawns on `127.0.0.1:50000`, forwards `/pl:` to a plugin's own HTTP routes (webhooks, callbacks), and falls through to Core API for everything else. - **Redis service discovery.** Every backend registers `erxes-service-` (its address) and `erxesservice:config:` (Mongo URL, `meta`, release version) through [service-discovery.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/service-discovery.ts). The gateway waits for `core` plus every name in `ENABLED_PLUGINS` before it composes the supergraph and starts listening. - **Subdomain tenancy.** The gateway derives the tenant from the request hostname; each service builds per-tenant Mongoose models from that subdomain, so the same code serves every organization. - **Module Federation frontend.** `core-ui` (port `3001`) loads `_ui/config` for each enabled plugin and builds routes, navigation, settings, and widgets from the exported `IUIConfig`. In production the remote entries come from the `plugins.erxes.io` CDN, not from your containers (see [bootstrap.tsx](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/bootstrap.tsx)). - **Shared libraries with hard boundaries.** Backend code uses `erxes-api-shared` (`utils`, `core-types`, `core-modules` entrypoints); frontend code uses `erxes-ui` and `ui-modules`, always through public exports, never another service's source. - **Background services are not subgraphs.** `logs` (`3301`) and `automations` (`3302`) are BullMQ workers the gateway never routes GraphQL to. Plugins reach them through `meta` registrations and named `-` queues on Redis. ## Request flow ```text Browser ── HTTPS ──> Nginx ── /gateway/ ──> Gateway :4000 │ userMiddleware maps Bearer / app / │ portal / agent credentials into │ user, clientportal, cpuser headers ├─ /graphql ──> Apollo Router 127.0.0.1:50000 │ ├─> Core API :3300 (core subgraph) │ └─> Plugin APIs :3303+ (subgraphs) ├─ /pl: ──> plugin HTTP routes (webhooks) └─ / ──> Core API routes (/initial-setup, /get-frontend-plugins, ...) Every service registers in Redis: erxes-service-, erxesservice:config: BullMQ queues on Redis ──> logs-service :3301, automations-service :3302 core-ui :3001 host loads _ui remotes via Module Federation (production remote entries served from plugins.erxes.io) ``` ## Guides - [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway): Core API, plugin APIs, and federated gateway composition - [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend): Core UI host and Module Federation remotes - [Shared Libraries](https://erxes.io/docs/architecture-shared-libraries): Reuse erxes-api-shared, erxes-ui, and ui-modules - [Background Services](https://erxes.io/docs/architecture-background-services): Automations and logs workers with BullMQ and Redis --- # Backend & API Gateway Core API, plugin APIs, and federated gateway composition How Core API, plugin APIs, and the gateway compose one federated GraphQL endpoint. Complete [Local Setup](https://erxes.io/docs/local-setup) before running these services. For deployment images, see [Self-Hosting](https://erxes.io/docs/self-hosting). ## Services and ports | Service | Project | Port | Notes | | --- | --- | --- | --- | | Gateway | `backend/gateway` | `4000` (`PORT`) | Public API entry point | | Core API | `backend/core-api` | `3300` (`PORT`) | `core` subgraph + REST routes | | Logs service | `backend/services/logs` | `3301` (`PORT`) | BullMQ workers, no subgraph | | Automations service | `backend/services/automations` | `3302` (`PORT`) | BullMQ workers + tRPC | | Plugin APIs | `backend/plugins/*_api` | `3303`-`3313` | e.g. sales `3305`, frontline `3304`, operation `3307` | | Apollo Router | spawned by the gateway | `127.0.0.1:50000` (`APOLLO_ROUTER_PORT`) | Loopback only; never publish | ## Request flow 1. The browser calls `https:///gateway/...`, which a reverse proxy forwards to the gateway on port `4000` with the `/gateway` prefix stripped. See [Self-Hosting](https://erxes.io/docs/self-hosting) for the Nginx configuration. 2. `backend/gateway/src/main.ts` applies a global rate limit (5000 requests per 15 minutes per IP, skipping `/health` and `/bullmq-board`), then CORS. A request carrying an active app token in `x-app-api-token` gets `origin: true`; all others get the `DOMAIN`, `WIDGETS_DOMAIN`, `ALLOWED_DOMAINS`, and `ALLOWED_ORIGINS` allow-list. 3. `userMiddleware` identifies the caller (see below). 4. Routes dispatch as follows: - `^/graphql` proxies to the Apollo Router at `http://127.0.0.1:50000`, which executes the query across subgraphs. - `/pl:` proxies directly to the address that `` registered in Redis, bypassing the router. Plugins use this for HTTP routes such as webhooks. - `/graphql` over WebSocket is served by the gateway's own subscription server (see below). - `/` falls through to Core API (`core.address` in production, `http://localhost:3300` in development). Core routes such as `/initial-setup`, `/get-frontend-plugins`, and `/subscriptionPlugin.js` are reached this way. - `/health` returns `ok`; `/bullmq-board` serves a Bull Board UI for the `gateway-service-discovery` queue; `/locales/:lng/:file` serves plugin translation JSON. The proxy layer adds `hostname`, `userid`, `x-forwarded-for`, `x-forwarded-port`, and `x-forwarded-proto` headers to every forwarded request. ## Service discovery in Redis Every backend service announces itself by writing two Redis keys after it starts listening: - `erxes-service-`: the service's HTTP address. - `erxesservice:config:`: JSON with `dbConnectionString`, `hasSubscriptions`, `meta` (merged with any existing meta), and `releaseVersion`. Note the key is `erxesservice` with no hyphen. The address is `LOAD_BALANCER_ADDRESS` when set, otherwise `http://localhost:` in development and `http://plugin--api:` in production. `joinErxesGateway` in [service-discovery.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/service-discovery.ts) performs this registration. The gateway additionally stores the enabled plugin list under `erxes-active-plugins`. `releaseVersion` comes from `RELEASE_VERSION` when it starts with `3.`; otherwise it is `latest`. Core API uses it to version frontend plugin asset URLs; see [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend). ## Supergraph composition On `start()`, the gateway: 1. Calls `getPlugins()`, which returns `['core', ...ENABLED_PLUGINS, ...ENABLED_PLUGINS_ONLY_API]`. 2. For each name, waits until `erxes-service-` exists (`retryGetProxyTarget`, one attempt per second) and until `POST
/graphql` answers the federation introspection query `_service { sdl }` (`retryEnsureGraphqlEndpointIsUp`, one attempt per 5 seconds). `MAX_PLUGIN_RETRY` bounds these retries; unset means retry forever. 3. Writes a Rover config listing every target as a subgraph (`federation_version =2.9.3`, routing and schema URL `
/graphql`) and runs `rover supergraph compose` to produce `supergraph.graphql` under `src/apollo-router/temp/` ([supergraph-compose.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/apollo-router/supergraph-compose.ts)). 4. Spawns Apollo Router `v1.59.2` with a generated `router.yaml`: 300-second timeouts, `include_subgraph_errors`, all request headers propagated, `cors.allow_credentials`, a Rhai script that merges subgraph `set-cookie` and `server-timing` headers into the response, and introspection enabled in development or when `INTROSPECTION=true`. 5. Waits for a TCP connection to the router port, then starts listening on `4000`. If the router exits unexpectedly, the gateway respawns it with a growing backoff (up to 30 seconds). In development, the gateway re-runs `rover supergraph compose` every `SUPERGRAPH_POLL_INTERVAL_MS` (default `10000`) so plugin restarts update the schema without a gateway restart. In development it also downloads the router binary on first run, which needs internet access; the production image already contains Rover and the router (see the [gateway Dockerfile](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/Dockerfile)). In production, `joinErxesGateway` instead enqueues a delayed BullMQ job on the `gateway-update-apollo-router` queue (10-second delay, 3 attempts, guarded by a 30-second `gateway:update-apollo-router:pending` lock so a burst of registrations triggers one recompose). The gateway's `update-apollo-router` worker ([workers.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/mq/workers/workers.ts)) clears the service-discovery cache, refetches targets, recomposes, and restarts the router. ## What happens when a plugin starts [start-plugin.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/start-plugin.ts) runs this sequence for every `*_api` service: 1. Builds the Express app with CORS, JSON body parsing (`15mb` limit), cookies, `/health`, and `/debug-sentry`. 2. Mounts the plugin's `expressRouter`, `middlewares`, and `apiHandlers` (each wrapped in `logHandler`, which reports to the logs queue when a logs service is registered). 3. When `hasSubscriptions` is set, serves `/subscriptionPlugin.js` behind a rate limiter. When `trpcAppRouter` is set, mounts `/trpc`. 4. Builds a subgraph schema from the plugin's `graphql()` typeDefs and resolvers (wrapped by `wrapApolloResolvers` for auth and error handling) and mounts it at `/graphql`. 5. Listens on the configured `PORT`, then calls `joinErxesGateway`. This is when the service becomes visible to the gateway. 6. Processes `meta`: `beforeResolvers`, `afterProcess`, `references`, `automations`, `segments`, `notifications`, `importExport`, and `payments` each register producers or write plugin config back to `erxesservice:config:`. Core API follows the same pattern but with its own bootstrap in [main.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/main.ts): it mounts `src/routes` (which includes `/initial-setup` and `/get-frontend-plugins`), starts Apollo, then registers as `core` with `hasSubscriptions: true`. ## Authentication and tenants `userMiddleware` ([userMiddleware.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts)) runs on every request before routing, in this order: 1. `erxes-core-token` + `erxes-core-website-url`: validates against `https://erxes.io/check-website` and grants a limited system principal. 2. `erxes-app-token`: looks up an active `Apps` record; a valid token creates a synthetic user `app:` (owner when `allowAllPermission`). A separate `x-app-api-token` header is checked earlier, in the CORS layer, where a valid token opens `origin: true` for the request and is cached for one hour under `app_token::`. 3. `x-app-token`: a JWT signed with `JWT_TOKEN_SECRET` identifying a client portal; adds a base64 `clientportal` header. `client-auth-token` (header or cookie) also identifies a portal user and adds a base64 `cpuser` header. 4. `Bearer` token or `auth-token` cookie: a JWT signed with `JWT_TOKEN_SECRET`. The user must exist in the tenant's `Users` collection and the token must still be listed in Redis as `user_token__`. The user document travels downstream as a base64 `user` header (with `loginToken` and bulky fields stripped) plus a `userid` header. OAuth access tokens (`typ: 'oauth_access'`) also carry `oauthClientId` and scopes. The tenant is derived from the hostname: `getSubdomain` reads the `nginx-hostname` header, then the `hostname` header, then `req.hostname`, and takes the first DNS label. Each service uses that subdomain to select tenant models. See [Authentication](https://erxes.io/docs/api-authentication). ## Gateway environment variables | Variable | Default | Purpose | | --- | --- | --- | | `PORT` | `4000` | Gateway listen port | | `NODE_ENV` | none | `development` enables dev remotes, router download, polling recompose | | `APOLLO_ROUTER_PORT` | `50000` | Loopback port for the internal router | | `DOMAIN` | `http://localhost:3000` | Primary UI origin for CORS | | `WIDGETS_DOMAIN` | `http://localhost:3200` | Widgets origin for CORS | | `ALLOWED_DOMAINS` | none | Extra comma-separated CORS origins | | `ALLOWED_ORIGINS` | none | Comma-separated regexes added to the CORS origin list | | `JWT_TOKEN_SECRET` | `SECRET` | Verifies user, portal, and app JWTs; always set in production | | `MONGO_URL` | `mongodb://127.0.0.1:27017/erxes?directConnection=true` | App/user lookups in `userMiddleware` | | `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` | `6379` | Shared Redis for discovery, queues, tokens | | `ENABLED_PLUGINS` / `ENABLED_PLUGINS_ONLY_API` | none | Plugin names the gateway must wait for | | `LOAD_BALANCER_ADDRESS` | none | Address each service advertises instead of the derived default | | `RELEASE_VERSION` | none | Values starting with `3.` become the plugin asset version | | `VERSION` | `os` | `os` or `saas`; changes plugin discovery and CORS paths | | `INTROSPECTION` | none | `true` enables GraphQL introspection in production | | `MAX_PLUGIN_RETRY` | unlimited | Bounds plugin join/GraphQL readiness retries | | `SUPERGRAPH_POLL_INTERVAL_MS` | `10000` | Dev-mode recompose interval | | `GRAPHQL_LIMITER` | none | When set, enables depth/alias/character limits | | `TRUST_PROXY` | `loopback, linklocal, uniquelocal` | Express `trust proxy` setting | | `DEBUG_GATEWAY_AUTH` | `false` | Verbose auth/proxy request logging | --- # 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/_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 `` 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 `_ui` with dashes converted to underscores (federation container names cannot contain dashes). - The entry is `https://plugins.erxes.io//_ui/remoteEntry.js`. `` 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 `_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('/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 `//*` and lazy-loads `_ui/` through `RenderPluginsComponent`, guarded by `PermissionRouteGuard`. - Settings routes load `_ui/Settings`. - `resolveRemoteComponent` tries `default`, the PascalCase module name, `Component`, `RemoteEntry`, `RemoteEntries`, `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). --- # Shared Libraries Reuse erxes-api-shared, erxes-ui, and ui-modules What to reuse from `erxes-api-shared`, `erxes-ui`, and `ui-modules` instead of reimplementing. For plugin patterns, see [Backend Plugins](https://erxes.io/docs/plugin-backend) and [Frontend Plugins](https://erxes.io/docs/plugin-frontend). For how these libraries connect to services and remotes, see [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway) and [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend). ## Import boundaries Every library is imported through package-name paths resolved by `paths` in [tsconfig.base.json](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/tsconfig.base.json): ```json "paths": { "erxes-ui": ["frontend/libs/erxes-ui/src/index.ts"], "ui-modules": ["frontend/libs/ui-modules/src/index.ts"], "erxes-api-shared": ["backend/erxes-api-shared/src/index.ts"], "erxes-api-shared/*": ["backend/erxes-api-shared/src/*"] } ``` The rule: **use only public interfaces.** Import `erxes-api-shared/utils`, `erxes-api-shared/core-types`, or `erxes-api-shared/core-modules`; never reach into a file's internals, copy shared source into a plugin, or import one service's implementation from another. Cross-service communication goes through the published GraphQL, tRPC, HTTP, and event APIs, or Module Federation exposes. Frontend plugins may not import from each other at all. ## Backend: erxes-api-shared Location: `backend/erxes-api-shared`. Its [package.json](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/package.json) declares four Preconstruct entrypoints, which define the public API: | Import | Contents | | --- | --- | | `erxes-api-shared` | Root entry; effectively empty, use the three subpath entries below | | `erxes-api-shared/utils` | Service bootstrap and infrastructure: `startPlugin`, `joinErxesGateway`, `redis`, `mq-worker` (`sendWorkerQueue`, `createMQWorkerWithListeners`), `mongo` (connection, `createGenerateModels`, cursor utils), `apollo` (`wrapApolloResolvers`, `generateApolloContext`, pagination), `trpc`, `headers` (user/subdomain propagation), `logs` (`logHandler`, after-process), `file` (read/upload), `email`, `saas`, `sentry-init`, `graphqlPubSub`, `getEnv`, `getSubdomain`, `applyTrustProxy`, `createHealthRoute` | | `erxes-api-shared/core-types` | Shared TypeScript types: `IMainContext`, `IPermissionConfig`, document interfaces, charge types | | `erxes-api-shared/core-modules` | Domain modules and engines a plugin plugs into: `automations`, `segments`, `logs`, `notifications`, `properties`, `permissions`, `forms`, `apps`, `users`, `approval`, `client-portals`, `import-export`, `templates`, `common` | The library compiles with Preconstruct (`pnpm nx build erxes-api-shared`) into `dist/` plus the `utils/`, `core-types/`, and `core-modules/` entrypoint directories, which the service Dockerfiles copy into `node_modules/erxes-api-shared`. > [!WARNING] > **Rebuild after every change** > > Services use the compiled output, not `src/`. After editing anything under `backend/erxes-api-shared/src`, run `pnpm nx build erxes-api-shared` before starting or rebuilding dependent services, or they keep running the stale build. ## Frontend: erxes-ui and ui-modules | Library | Path | Exports | | --- | --- | --- | | `erxes-ui` | `frontend/libs/erxes-ui` | Design-system primitives (`components`, `lib`, `utils`), `hooks`, `state`, `types` (including `IUIConfig`), `constants`, and product building blocks under `modules/`: `board`, `record-table`, `filter`, `inputs`, `layout`, `display`, `attachments`, `record-field`, `select-tree`, `side-menu`, `preview`, `report`, `navigation-menu`, `focus-sheet`, `hotkey`, `icons`, `motion`, `blocks`, `chat-viz`, `select-operation`. Also the shared env config (`REACT_APP_API_URL`, `NODE_ENV`) the host and remotes read | | `ui-modules` | `frontend/libs/ui-modules` | Business-domain UI under `modules/`: `contacts`, `products`, `sales`, `segments`, `tags`, `tags-new`, `automations`, `activity-logs`, `internal-notes`, `notifications`, `permissions`, `properties`, `documents`, `favorites`, `import-export`, `approval`, `brands`, `structure`, `team-members`, `templates`, `header`, `widget`, `payments`, `shared`, `types`, plus `states` (including `pluginsConfigState`) and `hooks` | Both are Module Federation shared singletons: the host and every remote share one instance, which is why Jotai state and React context written by `ui-modules` are visible to plugin code. Use `erxes-ui` for primitives, `ui-modules` for domain composition, and `@tabler/icons-react` for icons; do not hand-roll a parallel component set. The shared-dependency list lives in two places: [core-libraries.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/core-libraries.ts) at the repository root (the canonical set, including `radix-ui`) and the `coreLibraries` copy inlined in each project's `module-federation.config.ts`. The plugin generator writes the same set into every new remote. ## Path aliases - Frontend plugins: `~/*` → `src`, `@/*` → `src/modules`, plus `erxes-ui` and `ui-modules`. - Backend services and plugins: `~/*` → `src`, `@/*` → `src/modules`, `erxes-api-shared/*` → the shared library source. --- # Background Services Automations and logs workers with BullMQ and Redis When to run the `logs-service` and `automations-service` workers alongside the APIs, and how work reaches them. For local startup, see [Local Setup](https://erxes.io/docs/local-setup). For queue inspection, see [Logs & Troubleshooting](https://erxes.io/docs/self-hosting-troubleshooting). For gateway registration mechanics, see [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway). ## The two services | Service | Nx project | Port | Registers in Redis | Queues read | | --- | --- | --- | --- | --- | | Logs | `logs-service` (`backend/services/logs`) | `3301` | `erxes-service-logs` | `logs-put_log`, `logs-activity_log`, `logs-segment-{changed,forget,rebuild,reconcile}` | | Automations | `automations-service` (`backend/services/automations`) | `3302` | `erxes-service-automations` | `automations-trigger`, `automations-action`, `automations-aiAgent` | Both are plain Express apps exposing `/health` (JSON via `createHealthRoute`) but **no GraphQL subgraph**. They do not appear in `getPlugins()`, so the gateway never waits for them or routes GraphQL to them. Each registers itself manually on startup: the address under `erxes-service-` and `{"dbConnectionString": MONGO_URL}` under `erxesservice:config:`, using `LOAD_BALANCER_ADDRESS` when set. The automations service additionally mounts webhook routes (`webhookRoutes`) and a `/trpc` router serving the `automations.trigger` and `automations.completeDeferredAction` procedures that plugins call back into. ## How work reaches the queues All queues are BullMQ queues on the shared Redis, named `-`. Producers call `sendWorkerQueue(service, queue)`; workers call `createMQWorkerWithListeners(service, queue, handler)`: both live in [mq-worker.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mq-worker.ts). Notable producers: - `sendWorkerQueue('logs', 'put_log')`: event/audit log entries written by `logHandler`, which wraps plugin `apiHandlers` and log-enabled code paths. `logHandler` first checks `checkServiceRunning('logs')` (the `erxes-service-logs` key); with no logs service registered, the work still executes but nothing is persisted. - `sendWorkerQueue('logs', 'activity_log')`: activity feed entries. - `sendWorkerQueue('logs', 'segment-*')`: segment membership jobs produced through `SEGMENT_QUEUES` in `core-modules/segments`. - `sendWorkerQueue('automations', 'trigger')`: automation trigger dispatch from `sendAutomationTrigger` in `core-modules/automations`. - `sendWorkerQueue('gateway', 'update-apollo-router')`: production supergraph recomposition; see [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway). The gateway also creates a `gateway-service-discovery` queue for its Bull Board UI at `/bullmq-board`. ## How plugins register automation and log behavior Plugins declare background behavior in the `meta` object passed to `startPlugin`: no direct queue access needed: - `meta.automations`: `startAutomations` writes the config into `erxesservice:config:.meta.automations` and mounts typed tRPC producers on the plugin's `/trpc` router (`receiveActions`, `setProperties`, `checkCustomTrigger`, `findObject`, `generateAiContext`, `loadAiKnowledgeDocumentBatch`, `lookupAiTool`). The automations service reads every plugin's meta from Redis (`getPlugin(name).config.meta.automations`) and calls these procedures while executing triggers and actions. - `meta.afterProcess`: registers `afterMutation`, `afterAuth`, `afterApiRequest`, `afterDocumentUpdated`, and `afterDocumentCreated` producers the same way. - `meta.logs`: declares log content types so event and activity entries are labeled correctly. - `meta.segments`: `initSegmentProducers` registers the plugin's segment producers. - `meta.importExport`: starts an in-process import/export worker on the plugin. ## Data layout The logs service resolves tenant models per subdomain like an API, then splits storage: - `activityLogs` and `users` live in the tenant database (same as the app DB). - `logs` documents go to a separate database named `_logs` via `db.useDb(...)` in [connectionResolvers.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/services/logs/src/connectionResolvers.ts); for `MONGO_URL=.../erxes`, that is `erxes_logs`. This is also where the automatic undo journal lands: writes through `erxes-api-shared` models are journaled to `_logs` so System Logs → Undo can revert updates and deletes. The journal capture always runs; without the logs service, entries are queued but never persisted. `LOG_RETENTION_DAYS` (default `365`) sets the log document TTL. ## Enabling services locally [scripts/start-api-dev.js](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/start-api-dev.js) (behind `pnpm dev:apis`) reads three variables and runs `nx run-many -t serve -p core-api _api -service gateway`: ```dotenv ENABLED_PLUGINS=sales ENABLED_PLUGINS_ONLY_API=posclient ENABLED_SERVICES=automations,logs ``` `ENABLED_SERVICES` entries are mapped to `-service` Nx projects (`automations` → `automations-service`). Use base names with no `_api`/`_ui`/`_service` suffixes, and restart `pnpm dev:apis` after changing the list. ## Production notes CI publishes `erxes/erxes-next-logs` and `erxes/erxes-next-automations` images. Deploy a container per service you need with the same `MONGO_URL`, `REDIS_*`, `JWT_TOKEN_SECRET`, and `DOMAIN` environment as the APIs. A gateway plus Core API alone does not persist logs or execute automations. `ENABLED_SERVICES` is a development-startup variable; it does not create containers. Segment worker concurrency is tunable per queue: `SEGMENT_CONCURRENCY_CHANGED` (10), `SEGMENT_CONCURRENCY_FORGET` (5), `SEGMENT_CONCURRENCY_REBUILD` (1), `SEGMENT_CONCURRENCY_RECONCILE` (3). --- # Deploy & Operate Deployment, configuration, migrations, upgrades, and troubleshooting for self-hosted erxes Deploy erxes on your own infrastructure and keep it running: installation, environment variables, database migrations, upgrades, and troubleshooting. For running the source on a laptop instead, use [Local Setup](https://erxes.io/docs/local-setup). The reference deployment is a single-server Docker Compose stack (Core UI, Core API, gateway, logs-service, and Redis) behind an Nginx HTTPS proxy, backed by a MongoDB replica set you provision yourself. The repository includes Dockerfiles and CI workflows but no Compose file or installer; [Deployment](https://erxes.io/docs/self-hosting) derives a working template from them, and the remaining guides cover the lifecycle after first boot. ## Before you deploy - **MongoDB replica set.** Required, not optional: plugins use sessions and transactions (some wrap writes in `startSession`). Your `MONGO_URL` must resolve every advertised replica member from inside the containers and grant access to both the application database and its `_logs` database, which the logs service writes to. - **Redis.** A persistent `redis:7` instance on the private network (the template enables AOF and `noeviction`). Redis holds service-discovery keys, user session tokens, and all BullMQ queues; losing it drops queued work and logs everyone out. There is no `REDIS_URL`; services read `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD`. - **`DOMAIN` and `GATEWAY_URL`.** Set to `https://` and `https:///gateway`. They set the gateway's CORS allow-list and the links the platform generates. The Core UI container needs `REACT_APP_API_URL` pointed at the same gateway URL. - **`JWT_TOKEN_SECRET`.** One random value, identical on the gateway, Core API, every plugin API, and the background services; it signs team-member sessions, portal tokens, and app tokens. A mismatch silently invalidates logins. - **Pinned image tags.** Prefer `:` release tags or `repository@sha256:` digests over floating `latest` for the `erxes/erxes-next-*` images, so rollback is a config change. The [release workflow](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/release.yml) re-tags the image matrix on each release tag. `erxes/erxes-next-migrations` is not in that matrix; pin it by `latest` or digest. - **`LOAD_BALANCER_ADDRESS` per service.** Without it a production service registers `http://plugin--api:` in Redis, which must resolve inside your Docker network. - **`ENABLED_PLUGINS` on both Core API and the gateway.** Comma-separated base names (`sales,operation`). The gateway waits for every listed API to register and serve its subgraph before it listens. Deploy every listed plugin, or remove it from the list. ## Guides - [Deployment](https://erxes.io/docs/self-hosting): Deploy erxes on your own server with Docker Compose: requirements, images, and first run - [Configuration](https://erxes.io/docs/self-hosting-configuration): Environment variables behind the deployment template - [Database Migrations](https://erxes.io/docs/self-hosting-migrations): Back up and migrate MongoDB across versions - [Upgrades](https://erxes.io/docs/self-hosting-upgrades): Choose compatible images and roll back safely - [Logs & Troubleshooting](https://erxes.io/docs/self-hosting-troubleshooting): Diagnose health, composition, and plugin loading --- # Deployment Deploy erxes on your own server with Docker Compose: requirements, images, and first run Deploy erxes on your own server using Docker Compose and an HTTPS reverse proxy: a **single-server Core UI, Core API, gateway, and logs-service deployment**, backed by a MongoDB replica set and persistent Redis. Plugins and external integrations are added after the core installation is working. The repository does not include a ready-made Compose file. The configuration below is a deployment template built from the Dockerfiles, CI workflows, and runtime code in [erxes/erxes](https://github.com/erxes/erxes), not a bundled installer or a high-availability setup. Supply a working MongoDB endpoint and compatible image references before running it. For running source code on your laptop, use [Local Setup](https://erxes.io/docs/local-setup). ## Deployment layout ```text Browser | | HTTPS :443 v Host Nginx |-- / --> 127.0.0.1:3000 --> Core UI container :80 |-- /gateway/ --> 127.0.0.1:4000 --> Gateway container :4000 | | GraphQL / service proxy v Core API :3300 Core API, gateway, and logs-service --> MongoDB replica set Core API, gateway, and logs-service --> Redis :6379 (private Docker network) ``` The gateway image ([Dockerfile](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/Dockerfile)) bundles Apollo Router `v1.59.2` and Rover `0.22.0` for GraphQL composition. Core API registers its internal address through Redis. Apollo Router's internal port defaults to `127.0.0.1:50000`; it does not need a published host port. See [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway) for the full flow. ### 1. Prepare the server Use an **x86-64 Linux server**, such as Ubuntu 24.04 LTS. The gateway, Core API, automations, and migrations images are published for `linux/amd64` only; the logs and plugin API images also publish `linux/arm64`. The Compose example pins `platform: linux/amd64` so it behaves identically on ARM hosts that can emulate. Install: - [Docker Engine](https://docs.docker.com/engine/install/ubuntu/) and the [Docker Compose plugin](https://docs.docker.com/compose/install/). - Nginx, with Certbot and its Nginx plugin for HTTPS. - `curl` and OpenSSL for checks and secret generation. This guide uses Ubuntu's `sites-available`/`sites-enabled` layout and `systemctl`. Adjust those commands for another distribution. For capacity: the repository does not establish a tested minimum CPU, RAM, or disk size. Budget for the Node.js services, Redis, container images, uploads, and your database workload, and measure a staging deployment before choosing production capacity. For networking: - Point your application hostname, such as `erxes.example.com`, at the server. - Allow inbound HTTPS (`443`) and HTTP (`80`, used for certificate issuance and redirects), plus your administration access. - The example publishes the UI and gateway ports on loopback only. Core API, Redis, and the router stay internal. - Allow the containers to reach your MongoDB servers. With plugins enabled, browsers also need access to the plugin asset host described in step 8. Create a deployment directory owned by your deployment account; all Compose commands below run from it: ```bash mkdir -p ~/erxes-deploy cd ~/erxes-deploy ``` ### 2. Prepare MongoDB Provision a MongoDB replica set before starting erxes: a self-managed set on private infrastructure or a managed MongoDB service. Replica sets support the sessions and transactions some plugins use (`startSession` writes). A single-node set works for evaluation but provides no redundancy. Obtain a connection URI that: - Selects your application database, for example `erxes`. - Includes authentication and the appropriate `authSource`, replica-set, and TLS settings for your database. - Resolves to hosts reachable **from the Docker containers**, including all replica-set members MongoDB advertises. - Grants access to the application database **and** the logs database. The logs service writes to a separate database named `_logs`, such as `erxes_logs`; see [Background Services](https://erxes.io/docs/architecture-background-services). For a remote replica set, use its normal discovery URI rather than copying the `directConnection=true` URI from the local-development guide. URL-encode special characters in credentials. Verify the URI with `mongosh` from the deployment network before proceeding. Configure database persistence and automated backups with your database tooling. The Compose file manages Redis and erxes services; it does not provision MongoDB. ### 3. Select compatible container images CI publishes these image repositories: | Component | Image repository | Platforms | Tag patterns in CI | | --- | --- | --- | --- | | Core UI | `erxes/erxes-next-ui` | amd64 | `latest`, branch, short SHA, semver | | Core API | `erxes/erxes-next-core-api` | amd64 | `latest`, `YYYYMMDD-` | | Gateway | `erxes/erxes-next-gateway` | amd64 | `latest`, full commit SHA | | Logs service | `erxes/erxes-next-logs` | amd64, arm64 | `latest`, `YYYYMMDD-` | | Automations | `erxes/erxes-next-automations` | amd64 | `latest`, `YYYYMMDD-` | | Migrations | `erxes/erxes-next-migrations` | amd64 | `latest`, `YYYYMMDD-` | | Plugin APIs | `erxes/erxes-next-_api` | amd64, arm64 | `latest`, `YYYYMMDD-` | | Apps | `erxes/frontline-widgets`, `erxes/posclient-front`, `erxes/help-center` | amd64 | `latest`, branch, short SHA, semver | The [release.yml](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/release.yml) workflow additionally re-tags every image above **except `erxes/erxes-next-migrations`** as `:` when a release tag is pushed, so versioned tags exist for released versions. Prefer an explicit version tag or digest over floating `latest` so you can roll back; see [Upgrades](https://erxes.io/docs/self-hosting-upgrades). Record the selected references and digests: ```bash docker pull erxes/erxes-next-core-api:CHOSEN_TAG docker image inspect erxes/erxes-next-core-api:CHOSEN_TAG \ --format '{{json .RepoDigests}}' ``` Use the resulting `repository@sha256:...` reference in `.env`. Repeat for each service and check registry availability for your selected tags. If you build your own images, follow each Dockerfile and CI workflow for the version you deploy: Core API and the logs/automations Dockerfiles use prebuilt application and `erxes-api-shared` artifacts (run `pnpm nx build erxes-api-shared` and the service build first), while the gateway and Core UI Dockerfiles have their own build stages. ### 4. Configure the deployment Create `.env` in `~/erxes-deploy`. Replace every `REPLACE_...` value before starting: ```dotenv APP_HOST=erxes.example.com CORE_UI_IMAGE=REPLACE_WITH_CORE_UI_IMAGE_DIGEST CORE_API_IMAGE=REPLACE_WITH_CORE_API_IMAGE_DIGEST GATEWAY_IMAGE=REPLACE_WITH_GATEWAY_IMAGE_DIGEST LOGS_IMAGE=REPLACE_WITH_LOGS_IMAGE_DIGEST MONGO_URL=REPLACE_WITH_AUTHENTICATED_MONGODB_URI REDIS_PASSWORD=REPLACE_WITH_RANDOM_HEX JWT_TOKEN_SECRET=REPLACE_WITH_DIFFERENT_RANDOM_HEX ``` Generate a separate value for each secret with `openssl rand -hex 32`. > [!WARNING] > **Keep secrets private and consistent** > > Keep `.env` out of source control. All API services must share the same `JWT_TOKEN_SECRET`; it signs team-member sessions, portal tokens, and app tokens, and a mismatch silently invalidates logins. The same applies to `REDIS_PASSWORD` on every service and the Redis container itself. `APP_HOST` is a bare hostname; the Compose file adds `https://` when constructing `DOMAIN` and `GATEWAY_URL`. Create `compose.yml` alongside `.env`: ```yaml name: erxes x-api-environment: &api-environment NODE_ENV: production VERSION: os DOMAIN: https://${APP_HOST:?Set APP_HOST} GATEWAY_URL: https://${APP_HOST:?Set APP_HOST}/gateway MONGO_URL: ${MONGO_URL:?Set MONGO_URL} REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD} JWT_TOKEN_SECRET: ${JWT_TOKEN_SECRET:?Set JWT_TOKEN_SECRET} services: redis: image: redis:7 restart: unless-stopped environment: REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD} command: - sh - -c - exec redis-server --appendonly yes --maxmemory-policy noeviction --requirepass "$$REDIS_PASSWORD" volumes: - redis-data:/data healthcheck: test: ["CMD-SHELL", "REDISCLI_AUTH=$$REDIS_PASSWORD redis-cli ping | grep -q PONG"] interval: 10s timeout: 5s retries: 12 core-api: image: ${CORE_API_IMAGE:?Set CORE_API_IMAGE} platform: linux/amd64 restart: unless-stopped environment: <<: *api-environment PORT: "3300" LOAD_BALANCER_ADDRESS: http://core-api:3300 depends_on: redis: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3300/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 15s timeout: 5s retries: 20 start_period: 30s logs-service: image: ${LOGS_IMAGE:?Set LOGS_IMAGE} platform: linux/amd64 restart: unless-stopped environment: <<: *api-environment PORT: "3301" LOAD_BALANCER_ADDRESS: http://logs-service:3301 depends_on: redis: condition: service_healthy gateway: image: ${GATEWAY_IMAGE:?Set GATEWAY_IMAGE} platform: linux/amd64 restart: unless-stopped environment: <<: *api-environment PORT: "4000" INTROSPECTION: "false" ports: - "127.0.0.1:4000:4000" depends_on: core-api: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 15s timeout: 5s retries: 20 start_period: 60s core-ui: image: ${CORE_UI_IMAGE:?Set CORE_UI_IMAGE} platform: linux/amd64 restart: unless-stopped environment: NODE_ENV: production REACT_APP_API_URL: https://${APP_HOST:?Set APP_HOST}/gateway ports: - "127.0.0.1:3000:80" volumes: redis-data: ``` Notes on this file: - The backend reads `REDIS_HOST`, `REDIS_PORT`, and `REDIS_PASSWORD`; there is no `REDIS_URL`. Redis holds service-discovery keys and BullMQ queues, so the example enables AOF persistence and the `noeviction` policy. Pin the `redis:7` digest too when recording your release. - `LOAD_BALANCER_ADDRESS` must be reachable inside Docker. Without it, a production service registers `http://plugin--api:`, which does not match this template's service names. - `VERSION: os` selects the open-source code paths used by `/initial-setup` and plugin discovery. - The Core UI container's [entrypoint](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/docker-entrypoint.sh) writes every `REACT_APP_*` variable into a browser-readable `js/env.js` at startup. Put only public frontend configuration there; it is a runtime injection, not a build-time bake, so changing `REACT_APP_API_URL` takes effect on container restart. - `INTROSPECTION` stays `false` in production unless you need schema introspection for frontend development; the gateway warns when it is enabled. ### 5. Start the containers Validate the configuration without printing resolved credentials, pull the images, and start: ```bash docker compose config --quiet docker compose pull docker compose up -d docker compose ps docker compose logs --tail=100 core-api gateway logs-service ``` Wait for Core API to register and the gateway to compose its GraphQL schema, then check the loopback health endpoint: ```bash curl --fail http://127.0.0.1:4000/health ``` It returns `ok`. The gateway only listens after the router is up, so this also confirms initial schema composition; it does not prove every database, worker, or plugin is healthy. Complete the checks in step 7 after HTTPS is working. ### 6. Configure HTTPS with Nginx On Ubuntu, install Nginx and Certbot if needed: ```bash sudo apt update sudo apt install nginx certbot python3-certbot-nginx ``` Create `/etc/nginx/sites-available/erxes`, replacing `erxes.example.com` with your `APP_HOST`: ```nginx map $http_upgrade $erxes_connection_upgrade { default upgrade; '' close; } server { listen 80; server_name erxes.example.com; client_max_body_size 50m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location = /gateway { return 301 /gateway/; } location /gateway/ { proxy_pass http://127.0.0.1:4000/; proxy_http_version 1.1; proxy_buffering off; proxy_read_timeout 300s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $erxes_connection_upgrade; } } ``` The trailing slash in `proxy_pass http://127.0.0.1:4000/` strips the public `/gateway/` prefix, so `/gateway/graphql` reaches `/graphql` on the gateway. The WebSocket upgrade headers carry GraphQL subscriptions on the same path. `50m` is a proxy request limit, not a guarantee of application upload size; the JSON body limit in the APIs is `15mb`, and file uploads go through the configured storage backend. Enable the site, test it, and request a certificate: ```bash sudo ln -s /etc/nginx/sites-available/erxes /etc/nginx/sites-enabled/erxes sudo nginx -t sudo systemctl reload nginx sudo certbot --nginx -d erxes.example.com --redirect sudo certbot renew --dry-run ``` Certificate issuance requires working DNS and access to the HTTP challenge endpoint. Verify automatic renewal is scheduled. ### 7. Verify and create the owner account Check the public gateway and GraphQL endpoint: ```bash curl --fail https://erxes.example.com/gateway/health curl --fail https://erxes.example.com/gateway/graphql \ -H 'Content-Type: application/json' \ --data '{"query":"query SelfHostingHealth { __typename }"}' ``` Expect `ok` from health and a GraphQL response with `data.__typename` equal to `Query`. HTTP 200 with an `errors` array is not a successful GraphQL check. Open `https://erxes.example.com`. On a fresh database, the frontend fetches `/initial-setup`, sees `hasOwner: false`, and redirects to `/create-owner`. Create the initial owner account, sign in, then verify you can create and edit a record and that it appears without a manual refresh. Inspect browser requests for failed API calls or WebSocket connections. ### 8. Add plugins and integrations For each plugin, deploy its matching API image (`erxes/erxes-next-_api`) built from a compatible version, with the shared API environment plus `PORT` and `LOAD_BALANCER_ADDRESS`, and give `ENABLED_PLUGINS` to **both Core API and the gateway** as a comma-separated list of base names such as `sales,operation`, with no `_api`/`_ui` suffixes, spaces, or trailing commas. Deploy every listed API before recreating core services: the gateway waits for all enabled plugins to register and serve their subgraph. Plugin APIs that join after startup trigger a router recompose through the `gateway-update-apollo-router` queue, but a plugin that never registers leaves the gateway waiting at boot. For automation execution, deploy `erxes/erxes-next-automations` explicitly; `ENABLED_SERVICES` is a development-startup variable, not a Compose directive. The logs-service container in this template persists event logs, activity logs, and the undo journal; see [Background Services](https://erxes.io/docs/architecture-background-services). > [!WARNING] > **Plugin UI assets are served from a CDN** > > Core API's `/get-frontend-plugins` route returns `https://plugins.erxes.io//_ui/remoteEntry.js`; the asset host is hardcoded. `` is the plugin API's registered `releaseVersion` (`RELEASE_VERSION` when it starts with `3.`, else `latest`), and the corresponding folder must exist on the CDN. Self-hosting the backend does **not** make plugin assets local or the deployment offline-capable. Serving plugin UIs from your own infrastructure requires building the `*_ui` projects yourself and changing that route. Check the asset URLs and release compatibility before enabling plugins in production. See [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend). Configure the storage backend used by your deployment (Settings → file upload: `UPLOAD_SERVICE_TYPE` and provider credentials are stored in the database, not env vars) before relying on uploads, and include stored objects in backups. Email delivery, SSO, and other integrations need their own credentials; see [Configuration](https://erxes.io/docs/self-hosting-configuration). Widgets, the client portal template, the POS client, and the help center are separate applications with their own images (`erxes/frontline-widgets`, `erxes/posclient-front`, `erxes/help-center`) and environment; this template does not launch them. ## Operations ### Logs and troubleshooting ```bash docker compose ps docker compose logs --tail=200 gateway core-api logs-service docker compose logs --tail=100 redis core-ui ``` - **Gateway waits for a plugin:** the API container must be running, registered as `erxes-service-` in Redis, and reachable at its `LOAD_BALANCER_ADDRESS`; the enabled list must have no missing services. - **Gateway health works but GraphQL fails:** check the router process and `supergraph compose` errors in the gateway logs. - **MongoDB connection or authorization error:** check container DNS/connectivity, URI encoding, TLS, and permissions on both the application and `_logs` databases. - **Redis authentication error:** `REDIS_PASSWORD` must be identical on the container and every API/worker. - **Sign-in, CORS, or subscription failure:** check the public HTTPS URL, `DOMAIN`, `REACT_APP_API_URL`, proxy prefix stripping, and WebSocket upgrade headers. - **Plugin navigation fails:** check API registration plus the returned `plugins.erxes.io` asset URL and its version. - **Containers exit during startup:** check image architecture (`platform: linux/amd64`), compatible artifacts, env completeness, and memory. See [Logs & Troubleshooting](https://erxes.io/docs/self-hosting-troubleshooting) for more symptoms and fixes, including Redis discovery keys. ### Backups and upgrades Back up the application MongoDB database, its `_logs` database, uploaded files/object storage, and the deployment configuration. Preserve Redis state for queued work as your recovery plan requires. Store backups off the host and test restoration. Before an upgrade, review the release's migration notes ([Database Migrations](https://erxes.io/docs/self-hosting-migrations)), take a restorable backup, and test the new image set with a copy of your data. Then update the image references in `.env` and apply: ```bash docker compose pull docker compose up -d docker compose ps ``` Repeat the health, GraphQL, login, and plugin checks. See [Upgrades](https://erxes.io/docs/self-hosting-upgrades) for rollback. Restarting an older image does not reverse database migrations, and a single-server Compose update causes a short outage. ### Stop or remove the containers ```bash docker compose stop # stop, keep containers and volumes docker compose down # remove containers and network, keep named volumes ``` The `redis-data` volume survives `down`, and the separately managed MongoDB is unaffected. > [!WARNING] > **Data loss** > > `docker compose down -v` deletes the Redis volume (queues and service discovery). Run it only as a deliberate operation after confirming your MongoDB backups, never as part of generic host cleanup. ## Licensing Review the repository's [LICENSE.md](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/LICENSE.md) for the version you deploy. It contains AGPLv3 provisions, references Enterprise Edition terms, and prohibits hosting a SaaS version that competes with erxes Inc. Self-hosting does not by itself grant unrestricted plugin or commercial SaaS rights; Enterprise plugins are licensed separately. --- # Configuration Environment variables behind the deployment template Environment variables behind the [Self-Hosting](https://erxes.io/docs/self-hosting) deployment. For source development, use [Local Setup](https://erxes.io/docs/local-setup) and the repository's `.env.sample`. Many values can embed the literal string ``, which `getEnv` ([utils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/utils.ts)) replaces with the request tenant at runtime; useful for per-tenant URLs and keys. ## Runtime vs build-time Backend services read everything below from the container environment at startup; restart to apply changes. The Core UI container is different: its [entrypoint](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/docker-entrypoint.sh) writes every `REACT_APP_*` variable into a browser-readable `js/env.js` at startup, so `REACT_APP_*` values behave as **runtime** configuration for `erxes/erxes-next-ui` even though the same names are baked into the bundle as fallbacks. The standalone apps (`client-portal-template`, `help-center`, `posclient-front`) read `NEXT_PUBLIC_*` values at their own build or entrypoint time; see their app pages. ## Core runtime | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `NODE_ENV` | All backend + UI | Yes | none | `production` in deployment; `development` enables dev remotes, router download, and CORS extras | | `PORT` | Each service | No | `4000` gateway, `3300` core-api, `3301` logs, `3302` automations, `3303`+ plugins | Per-container override | | `VERSION` | core-api, gateway, service discovery | No | `os` | `os` or `saas`; switches `/initial-setup`, plugin discovery, and charge checks | | `RELEASE_VERSION` | Plugin APIs on registration | No | none | Values starting with `3.` become the `releaseVersion` used in plugin UI asset URLs; anything else becomes `latest` | | `LOAD_BALANCER_ADDRESS` | Every backend service | Recommended in production | derived | Address registered in Redis; defaults to `http://plugin--api:` in production, `http://localhost:` in dev | | `GET_CP_TOKEN` | core-api | Only to enable the route | none | Shared secret for `GET /get-client-portal-token?GET_CP_TOKEN=` | | `TEMPLATE_EXPORT_KEY` | core-api | For template export | none | Passphrase hashed to the AES-256-GCM key that encrypts template exports | ## Database | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `MONGO_URL` | All backend services | Yes | `mongodb://127.0.0.1:27017/erxes?directConnection=true` | Must reach all replica-set members from the containers; the logs service derives `_logs` from it | | `DB_NAME` | `erxes-api-shared` (saas) | saas only | `erxes_` | Tenant database name template | | `CORE_MONGO_URL` | `backend/saas-migrations` scripts | For those migrations | none | Some scripts accept a domain-specific override instead of `MONGO_URL` | ## Redis | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `REDIS_HOST` | All backend services | Yes | none | Shared ioredis client, BullMQ queues, GraphQL pubsub | | `REDIS_PORT` | All backend services | No | `6379` | none | | `REDIS_PASSWORD` | All backend services | When Redis requires auth | none | Must match the Redis container's `requirepass`; there is no `REDIS_URL` | | `SKIP_REDIS` | posclient sync utils | No | none | When set, POS config sync throws `token not found` instead of creating a config for an unknown token | Redis holds service discovery (`erxes-service-*`, `erxesservice:config:*`), session tokens (`user_token_*`), plugin lists (`erxes-active-plugins`), and every BullMQ queue. Persist it (`--appendonly yes`) and use `noeviction`. ## URLs, domains, and CORS | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `DOMAIN` | gateway, core-api, services | Yes | `http://localhost:3000` | Public UI origin; also the CORS allow-list anchor and the stored `hostname` fallback | | `GATEWAY_URL` | core-api email links | No | `${DOMAIN}/gateway` | Public gateway prefix used in emails, unsubscribe links, webhooks | | `WIDGETS_DOMAIN` | gateway, core-api | No | `http://localhost:3200` | Widgets origin added to CORS | | `ALLOWED_DOMAINS` | gateway, core-api, services | No | none | Extra comma-separated CORS origins | | `ALLOWED_ORIGINS` | gateway, core-api, services | No | none | Comma-separated **regexes** appended to the CORS origin list | | `CLIENT_PORTAL_DOMAINS` | logs, automations services | No | none | Extra origins for the background services' CORS | | `TRUST_PROXY` | All backend services | No | `loopback, linklocal, uniquelocal` | Express `trust proxy`; widen only if your LB appends client IPs | ## Auth and sessions | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `JWT_TOKEN_SECRET` | gateway, core-api, plugins | Yes | `SECRET` | Signs and verifies user, portal, and app JWTs. **Change it** and keep it identical on every backend service | | `SAME_SITE` | core-api | No | none | Set to `none` to emit cross-site auth cookies where the code supports it | | `WORKOS_API_KEY`, `WORKOS_PROJECT_ID` | core-api | For WorkOS SSO | none | SSO login mutations | | `OAUTH_DEVICE_VERIFICATION_URI` | core-api | No | built-in | OAuth device-flow verification page override | ## Plugin and service selection | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `ENABLED_PLUGINS` | gateway, core-api, dev scripts | For plugins | none | Comma-separated base names (`sales,operation`); controls gateway waits and `/get-frontend-plugins` | | `ENABLED_PLUGINS_ONLY_API` | gateway, dev scripts | No | none | Backend-only plugins without frontend remotes | | `ENABLED_SERVICES` | `scripts/start-api-dev.js` | Dev only | none | `automations,logs` → `*-service` Nx projects; **not** read by Docker images | | `MAX_PLUGIN_RETRY` | gateway | No | unlimited | Bounds plugin-join and `/graphql` readiness retries at startup | ## Gateway tuning | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `APOLLO_ROUTER_PORT` | gateway | No | `50000` | Loopback port for the internal router | | `INTROSPECTION` | gateway | No | off in production | `true` enables GraphQL introspection in production | | `GRAPHQL_LIMITER` | gateway | No | off | When set, applies depth/alias/character limits to `/graphql` | | `SUPERGRAPH_POLL_INTERVAL_MS` | gateway (dev) | No | `10000` | Dev-mode recompose interval | | `DEBUG_GATEWAY_AUTH` | gateway | No | `false` | Verbose auth, proxy, and limiter request logs | ## Workers and retention | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `LOG_RETENTION_DAYS` | `erxes-api-shared` logs schema | No | `365` | TTL index on the `_logs` database `logs` collection | | `REVERT_AUTO_JOURNAL_MAX` | `erxes-api-shared` | No | `1000` | Max documents snapshotted per bulk write for the undo journal | | `SEGMENT_CONCURRENCY_CHANGED` / `_FORGET` / `_REBUILD` / `_RECONCILE` | logs-service | No | `10` / `5` / `1` / `3` | BullMQ concurrency per segment queue | | `SEGMENT_RECONCILE_CRON` / `_TZ` / `_BATCH` / `_STEPS`, `SEGMENT_REBUILD_PAGE`, `SEGMENT_PREVIEW_BUDGET_MS`, `SEGMENT_TIME_ZONE` | segment engine | No | built-in | Segment sweep tuning | | `TIMEZONE` | sales board/POS report queries | No | `0` (hour offset) / `UTC` | Numeric hour offset in sales date math | ## Email | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `SENDGRID_API_KEY` | core-api auth emails | For SendGrid | none | Throws `SENDGRID_API_KEY is missing in environment` when a SendGrid path runs without it | | `SENDGRID_WEBHOOK_PUBLIC_KEY` | core-api broadcast trackers | For SendGrid event webhooks | none | Verifies inbound event signatures; a deployment-level key marks the webhook shared | | `AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`, `AWS_SES_CONFIG_SET`, `AWS_REGION` | broadcast transporter | For SES | none | SES sending path | | `DEFAULT_FROM_EMAIL` | `erxes-api-shared` email | No | none | Fallback sender; `COMPANY_EMAIL_FROM` system config overrides per tenant | | `EMAIL_DISPOSABLE_DOMAINS` | core-api intake | No | none | Extra disposable-domain list | | `EMAIL_PROVEN_WINDOW_DAYS` | core-api | No | `180` | Proven-address window | | `EMAIL_RAMP_TIERS`, `EMAIL_SOFT_BOUNCE_LIMIT` | core-api broadcast | No | built-in tiers / `3` | Sending ramp and bounce suppression | | `MAIL_DOMAIN`, `MAIL_WORKER_URL`, `MAIL_WEBHOOK_SECRET`, `MAIL_TENANT`, `MAIL_SENDING_ACCOUNT_ID`, `MAIL_SENDING_API_TOKEN`, `MAIL_SENDING_DAILY_LIMIT`, `MAIL_INBOUND_RATE_LIMIT`, `MAIL_RECEIVE_URL` | frontline_api mail channel | For the mail integration | none | Cloudflare Email Routing → worker → `/mail/receive`; see `.env.sample` and the frontline plugin guide | ## Frontend runtime (`REACT_APP_*`) Set on the `core-ui` container; injected into `window.env` at startup and falling back to build-time values: | Variable | Default | Notes | | --- | --- | --- | | `REACT_APP_API_URL` | `///gateway` (prod), `http://localhost:4000` (dev) | Public gateway URL the browser calls; `` in the value is replaced by the hostname's first label | | `REACT_APP_IMAGE_CDN_URL` | none | Image CDN base | | `REACT_APP_GOOGLE_MAP_API_KEY` | none | Maps integration | | `REACT_APP_HIDE_CORE_MODULES` | none | Hides built-in navigation modules | | `REACT_APP_SENTRY_DSN`, `REACT_APP_SENTRY_ENVIRONMENT` | none | Frontend Sentry | Variables on the standalone apps include `NEXT_PUBLIC_ERXES_API_URL` (`client-portal-template`, `help-center`; the latter injects it into `env.js` via its own `docker-entrypoint.sh`), `NEXT_PUBLIC_ERXES_CP_TOKEN` (`client-portal-template` only), and `NEXT_PUBLIC_MAIN_API_DOMAIN`, `NEXT_PUBLIC_APP_VERSION` (`posclient-front`). Check each app's Dockerfile/entrypoint: a container env override only works where the app injects runtime env; otherwise rebuild after changing them. ## Observability | Variable | Read by | Required | Default | Notes | | --- | --- | --- | --- | --- | | `SENTRY_DSN`, `SENTRY_ENVIRONMENT`, `SENTRY_RELEASE`, `SENTRY_SERVER_NAME`, `SENTRY_TRACES_SAMPLE_RATE` | All backend services | No | off | Sentry error tracing; tag as `plugin`/`service` | ## Not env vars Storage and upload settings are **system configs stored in the database**, read via `configs.getConfigs`: `UPLOAD_SERVICE_TYPE`, `AWS_BUCKET`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_COMPATIBLE_SERVICE_ENDPOINT`, `AWS_FORCE_PATH_STYLE`, `AWS_PREFIX`, `GOOGLE_CLOUD_STORAGE_BUCKET`, `GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_PROJECT_ID`, `CLOUDFLARE_*`, `AZURE_STORAGE_*`, `FILE_SYSTEM_PUBLIC`. Set them in Settings → file upload, not in `.env`. --- # Database Migrations Back up and migrate MongoDB across versions How erxes handles MongoDB schema and data changes during upgrades. For image selection and rollback planning, see [Upgrades](https://erxes.io/docs/self-hosting-upgrades). For the databases involved, see [Configuration](https://erxes.io/docs/self-hosting-configuration) and [Background Services](https://erxes.io/docs/architecture-background-services). ## How migrations work There is **no global migration framework**: no `migrate up` CLI, no version table, no automatic run on deploy. Migrations are standalone TypeScript scripts that each open their own MongoDB connection (reading `MONGO_URL`, sometimes a domain override like `CORE_MONGO_URL`), do their work, and call `process.exit()`. You run them deliberately, per release that needs one. Three kinds exist: 1. **Plugin-owned scripts**: live inside the plugin whose data they change, for example `backend/plugins/operation_api/src/migrations/` (`migrateTasks.ts`, `migrateCycle.ts`), `backend/plugins/frontline_api/src/migrations/` (ten scripts), and `backend/plugins/content_api/src/modules/cms/migrations/`. They are plain scripts you run with `tsx` against the deployment's `MONGO_URL`; there is no registration step. 2. **The consolidated `saas-migrations` package**: `backend/saas-migrations` holds one-off data migrations organized per domain (`core`, `content`, `frontline`, `operation`, `posclient`, `products`, `sales`, plus domains for Enterprise plugins). It includes a unified runner, [run.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/saas-migrations/run.ts), that discovers `*.ts` files under each domain directory and executes each in its own `node --import tsx` child process (required because the scripts self-invoke and exit). `core` runs first, then the rest alphabetically. 3. **`migrations.json` at the repository root**: an Nx workspace artifact listing code migrations for contributors upgrading Nx packages. It is not a product database mechanism; ignore it for deployment planning. A migration is only relevant if the release you are moving to includes one. Check the release notes and the `saas-migrations` directory diff for your version jump; most releases add no migration at all. ## Running the consolidated runner From a machine that can reach the database (a checkout, or the `erxes/erxes-next-migrations` image published by `ci-saas-migrations.yml`): ```bash # Preview order, run nothing tsx backend/saas-migrations/run.ts --list # Everything, core first; stops on the first failure tsx backend/saas-migrations/run.ts # Only some domains, or a single script tsx backend/saas-migrations/run.ts core frontline tsx backend/saas-migrations/run.ts sales/migrateSales.ts # Keep going past failures tsx backend/saas-migrations/run.ts --continue ``` Or from `backend/saas-migrations`: `pnpm migrate` and `pnpm migrate:list`. The runner exits non-zero if any script fails. ## Backup before anything writes Dump the application database **and** its logs database. The logs service writes to `_logs` (for example `erxes_logs`; see [connectionResolvers.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/services/logs/src/connectionResolvers.ts)) and undo history lives there: ```bash mongodump --uri="mongodb://:@/erxes?replicaSet=&authSource=admin" \ --db=erxes --archive=erxes-.archive.gz --gzip mongodump --uri="mongodb://:@/erxes?replicaSet=&authSource=admin" \ --db=erxes_logs --archive=erxes-logs-.archive.gz --gzip ``` Verify a restore on staging before relying on it: ```bash mongorestore --uri="" --archive=erxes-.archive.gz --gzip \ --nsFrom='erxes.*' --nsTo='erxes_restore.*' ``` Preserve Redis state too if queued BullMQ work matters to your recovery plan. ## Upgrade sequence with migrations ### 1. Record and back up Note the running image digests, take the dumps above, and verify the backup restores. ### 2. Check what the release ships Read `CHANGELOG.md` for your version jump and diff `backend/saas-migrations/` and plugin `src/migrations/` directories between the two tags. If nothing changed, no migration step is needed. ### 3. Stage it Restore production data to staging, deploy the candidate image set, run the relevant migration scripts, and exercise login, enabled plugins, automations, and file access. ### 4. Run in production Stop writes or use a maintenance window if the migration rewrites data. Run the scripts, then deploy the new image set together; do not mix a new Core API with old plugin APIs unless the release notes allow it. Watch the gateway, Core API, plugin, and logs-service output for migration or composition errors. ### 5. Verify Check `/health`, `/graphql`, owner login, and one write per enabled domain. Keep the pre-upgrade backup until the new version is confirmed. > [!WARNING] > **Migrations are not reversible by redeploying** > > Rolling the containers back to the previous image does not undo data changes a migration already wrote. Rollback after a data migration means restoring the pre-upgrade backup and losing every write since. Treat migration scripts as one-way and test them in staging first. --- # Upgrades Choose compatible images and roll back safely Upgrade a self-hosted installation without losing data or plugin compatibility. Back up first; see [Database Migrations](https://erxes.io/docs/self-hosting-migrations). For runtime settings, see [Configuration](https://erxes.io/docs/self-hosting-configuration). For the deployment this upgrades, see [Self-Hosting](https://erxes.io/docs/self-hosting). ## How releases and images are tagged erxes versions are cut with `release-it` (`pnpm release`): [.release-it.json](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.release-it.json) tags the release commit as `` (`tagMatch: 3.*`) and updates [CHANGELOG.md](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CHANGELOG.md) from conventional commits. Tag pushes then trigger [release.yml](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/release.yml), which re-tags a fixed matrix of Docker images (`erxes/erxes-next-core-api`, `erxes/erxes-next-gateway`, `erxes/erxes-next-ui`, `erxes/erxes-next-logs`, `erxes/erxes-next-automations`, all eleven `erxes/erxes-next-_api` images, plus `erxes/frontline-widgets`, `erxes/posclient-front`, and `erxes/help-center`) from `:latest` to `:` using `docker buildx imagetools create`. `erxes/erxes-next-migrations` is published by its own CI workflow but is **not** in the release matrix, so it has no `:3.1.x` tag; pin it by `latest` or digest. So a released version **does** produce matching `:3.1.9`-style tags across all core and plugin images, but each tag aliases whatever `:latest` was at release time. Between releases, CI also publishes floating `latest` plus per-service tags (`YYYYMMDD-` for APIs and services, full commit SHA for the gateway, branch/SHA/semver metadata tags for UI and apps). Prefer an explicit version tag or digest over `latest` so rollback is a config change, not a guess. Plugin **frontends** are not images: the `ci-ui-*` workflows sync each `*_ui` build to the `plugins.erxes.io` CDN under `latest/` or the release tag. Enabling a plugin version whose UI assets were not published breaks its navigation; see [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend). ## Reading the changelog: 3.1.6 to 3.1.9 `CHANGELOG.md` is the source of truth for what each release changed. For the 3.1.6 to 3.1.9 jump: - `3.1.7` added OAuth scope-per-plugin-module fixes, a compact `user` header with a 64 KB header budget, document locking, milestone/GitHub-issue sync, and a call-widget cache control. - `3.1.8` fixed call transfer, content Postiz delivery (tenant routing, JWT signing, QEMU-safe builds), MS Dynamics customer checks, and frontline surveys; it added conversation→ticket/deal/task conversion and `CALLPRO_SUBDOMAINS` gating. - `3.1.9` added "Basic information" system fields in Settings → Properties, member email activity logs, and frontline convert-property visibility. None of these releases document a required manual migration. The changelog only tells you what each release includes; confirm by diffing `backend/saas-migrations/` and plugin `src/migrations/` directories between your tags, as described in [Database Migrations](https://erxes.io/docs/self-hosting-migrations). ## Upgrade procedure ### 1. Choose a compatible image set List every image you run (Core UI, gateway, Core API, logs-service, each `*_api`, any apps), pick one version tag or digest for the whole set, and record current references for rollback: ```bash docker compose images docker images --digests | grep erxes ``` ### 2. Back up and stage Take a restorable `mongodump` of the application database and `_logs` ([Database Migrations](https://erxes.io/docs/self-hosting-migrations)). Restore it to staging, deploy the candidate image set with the production `.env` shape, run any new migration scripts, and verify login, enabled plugins, automations, file URLs, and customer-facing apps. ### 3. Upgrade production Announce the window; a single-server Compose update causes a short outage. Update the image references in `.env`, then: ```bash docker compose pull docker compose up -d docker compose ps ``` Wait for Core API to register and the gateway to recompose its supergraph (the `gateway-update-apollo-router` job handles plugins that join after startup). Run migration scripts for the release if it includes them. ### 4. Check it works Check `/health` on gateway and Core API, the `__typename` GraphQL probe, owner login, and one representative flow per enabled plugin, including the plugin's remote UI (`/get-frontend-plugins` must return reachable `remoteEntry.js` URLs). Keep the previous image digests and the pre-upgrade backup until the new version is stable. ## Rollback 1. Restore the previous image references in `.env` and run `docker compose up -d`. 2. Restore the pre-upgrade database backup **only** if a migration changed stored data and a forward fix is riskier; restoring loses writes made after the backup. 3. Re-verify health, GraphQL, login, and plugin UI loading. > [!WARNING] > **Rollback does not undo migrations** > > Recreating containers with older images reverts code, not data. If the release ran a data migration, the only safe rollback is the backup taken before it ran. --- # Logs & Troubleshooting Diagnose health, composition, and plugin loading Diagnose common self-hosted failures. Start with the service that logged the error: `docker compose logs -f gateway`, `... core-api`, `... logs-service`, `... -api`, or `... redis`. Health probes are `curl http://:4000/health` (gateway) and `curl http://:3300/health` (Core API); both return `ok` when healthy. ## Gateway and supergraph - **`/graphql` never becomes ready** (gateway logs `Waiting for plugin to join service discovery` or `WAITING FOR: graphql endpoint`): the gateway waits for every name in `getPlugins()` (`core` plus **both** enabled lists) to register and answer `_service { sdl }`. Compare `ENABLED_PLUGINS`/`ENABLED_PLUGINS_ONLY_API` with the plugin APIs actually deployed; remove entries whose API is not deployed, or deploy them. `MAX_PLUGIN_RETRY` bounds retries; once exhausted the gateway exits with code 1. - **`Supergraph composition failed`, `New supergraph could not be parsed`, `COLLISION`**: a subgraph's SDL is broken. Check the `rover supergraph compose` output on stderr in the gateway logs and each backend's `POST _service { sdl }` at its registered address, then fix or remove the offending subgraph. Composition runs in every gateway replica and the router reloads in place on success. - **A plugin registers but its GraphQL calls fail**: the address stored in `erxes-service-` is not reachable from the gateway (`LOAD_BALANCER_ADDRESS` was inferred as `http://plugin--api:` in production). Set `LOAD_BALANCER_ADDRESS` explicitly, or `DEL` the stale `erxes-service-` key and restart the plugin. - **Supergraph stays stale after a plugin restarted**: registration enqueues a `gateway-update-apollo-router` job delayed 10 seconds (3 attempts, exponential backoff, deduplicated by the `gateway:update-apollo-router:pending` lock). If the lock leaked, delete it and re-register a service. - **`EADDRINUSE :::4000` or `EACCES`**: the host port is already bound. Free it or remap `- "4001:4000"`; services do not retry bind failures. Check Redis state directly: `docker compose exec redis redis-cli -a KEYS 'erxes-service-*'` and `GET erxesservice:config:` (see [service-discovery.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/service-discovery.ts) for the key format). ## Auth and sessions - **Login works locally but fails behind HTTPS**: browsers drop the `secure` `auth-token` cookie (set whenever `NODE_ENV` is not `test`/`development`, with a 14-day `maxAge`) over plain HTTP. Serve the UI over HTTPS. Auth also accepts `Authorization: Bearer`, `erxes-core-token`, `erxes-app-token`, and `x-app-api-token` headers; `SAME_SITE=none` enables cross-site cookies when the origin differs from `DOMAIN`. - **`token auth failed` / `Invalid signature`**: `JWT_TOKEN_SECRET` differs between the gateway, core-api, and a plugin. Align the env and restart. The code default is `SECRET`; never deploy with it. - **Requests lose identity through `/pl:*` or `/graphql`**: the gateway repacks auth into a base64 `user` header (`setUserHeader` in [userMiddleware.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts)) and warns once it exceeds 32 KB; oversized headers get dropped by proxies. Set `DEBUG_GATEWAY_AUTH` to log `user-header-set`, `user-token-expired`, and `user-token-error` per request. A missing header means the request arrived unauthenticated; check token expiry and `user_token_*` Redis keys. - **Sessions expire or all users are logged out**: sessions are Redis-backed (`user_token__` keys, set with a 24-hour TTL at login). An eviction policy, flush, or Redis restart without AOF invalidates every session. Keep `noeviction` and persistence. ## Plugins missing or broken - **Plugin absent from navigation**: query `GET /get-frontend-plugins`; if the plugin is not listed with a `remoteEntry.js` URL, add the base name to `ENABLED_PLUGINS` and restart core-api. `VERSION=saas` additionally filters by purchased charge. - **`remoteEntry.js` 404**: the URL is `plugins.erxes.io//_ui/remoteEntry.js`, where `` comes from the plugin's `RELEASE_VERSION` registration. The `ci-ui-*` workflow must have published assets for that version; use a `RELEASE_VERSION` starting with `3.` or leave it unset for `latest`. - **A plugin route or widget fails to load in the browser**: check the DevTools network tab for the remote entry fetch and `loadRemote` errors in the console. The runtime remote name uses underscores (`pos-client` → `pos_client_ui`) while the CDN path keeps the original name; confirm the `remoteEntry.js` URL resolves publicly and the plugin's `config.tsx` name matches. - **`/pl:` returns `Service not found`**: the service name after `/pl:` must match the registered name, and `erxes-service-` must hold a reachable address; the gateway proxies `/pl:` to that address and rewrites the path to `/`. An empty or stale value produces the 404; fix `LOAD_BALANCER_ADDRESS` or delete the key and restart. ## Data services - **`Transaction numbers are only allowed on a replica set member` or `Transaction ... requires a replica set`**: `MONGO_URL` targets a standalone `mongod`, but some plugins wrap writes in `startSession` transactions. Initialize a replica set (`rs.initiate()`) and include `replicaSet=` in the URI. - **Backend cannot connect after fixing the URI**: with `directConnection=true` the URI's host must be reachable **from the containers**; do not mix `127.0.0.1` between host and container contexts. - **`WRONGPASS` / `NOAUTH` from Redis**: `REDIS_PASSWORD` does not match the Redis container's `requirepass`. Match them; the code only reads `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, with no `REDIS_URL`. - **Activity logs or undo history empty**: the logs service may not be running, or the `_logs` database is missing. Logs write to a **separate** database (`erxes` → `erxes_logs`); the `logs` collection TTL-deletes after `LOG_RETENTION_DAYS` (default 365). - **Automations never fire**: the `automations` service must be running and `erxes-active-plugins` must list it. Automations are triggered by `redisPubSub` events and BullMQ queues (`automations-trigger`, `-action`, `-aiAgent`); they are not a GraphQL subgraph. ## Networking and CORS - **Browser CORS errors**: the allow-list is exact origins (`DOMAIN`, `WIDGETS_DOMAIN`, comma-separated `ALLOWED_DOMAINS`) plus comma-separated **regexes** in `ALLOWED_ORIGINS`. There is no automatic subdomain wildcard; add tenant hosts via `ALLOWED_DOMAINS` or a regex. - **API calls go to the wrong host**: `REACT_APP_API_URL` on the `core-ui` container must be the **public** gateway URL the browser can reach. `` inside it is replaced by the hostname's first label at runtime. - **Wrong tenant resolved**: the subdomain is the hostname's first label, read from the `nginx-hostname` header, then the forwarded host; a flattened hostname (no labels) resolves to the base domain. - **Files upload but links break**: uploads are configured through Settings → file upload system configs (`UPLOAD_SERVICE_TYPE`, `AWS_*`, `CLOUDFLARE_*`, …), not env vars. Local `FILE_SYSTEM_PUBLIC` mode serves `read-file?key=` URLs. After any fix, re-run `docker compose ps`, `curl -fsS http://:4000/health`, the `__typename` GraphQL probe, a fresh login, and one write per enabled plugin. If composition still fails, run `rover supergraph compose` manually against the same `supergraph.yaml` inputs the gateway logs. --- # Core Platform Shared records, permissions, automations, and data exchange every plugin builds on The shared records and services every erxes plugin builds on: organizations and permissions, contacts, products, custom fields and segments, forms and documents, automations, broadcasts, email delivery, and bulk import/export. Core API (`backend/core-api`, port `3300`) is the always-on service behind the `core` subgraph. It registers with the gateway the same way a plugin API does, through its own bootstrap in [src/main.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/main.ts), but holds the platform's shared data model: the collections plugins link to rather than duplicate. Everything in this section is a core-api module or a shared pipeline it runs; plugin APIs reach them through shared service interfaces, never by reading core's collections directly. ## How plugins use core - **tRPC.** [init-trpc.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/init-trpc.ts) merges each module's router into the core `appRouter`, served at `/trpc`. A plugin builds a typed client through `getPlugin('core')`; the generator writes one into `src/trpc/trpcClients.ts`. It calls procedures such as `users.findOne` or `customers.count`. - **`meta` hooks.** The `meta` object a plugin passes to `startPlugin` (`permissions`, `automations`, `segments`, `notifications`, `importExport`, `afterProcess`, `references`) is serialized into the Redis key `erxesservice:config:` and read back by core-api, the gateway, and the background services. That is how a plugin adds permission actions, automation triggers and actions, segment types, notification types, and import/export handlers without touching core code. See [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions) for the full `meta` registration. - **Shared interfaces.** `erxes-api-shared/core-modules` holds the domain interfaces and producer helpers (`automations`, `segments`, `permissions`, `properties`, `forms`, `import-export`, and more) that both core and plugins compile against. ## Module map Each guide covers one area's data model, GraphQL API, permissions, and extension hooks. | Area | core-api location | Guide | | --- | --- | --- | | Team members, structure, brands, RBAC | `modules/organization`, `modules/permissions`, `modules/auth` | [Organizations, Users & Permissions](https://erxes.io/docs/core-organizations-users) | | Customers and companies | `modules/contacts` | [Contacts](https://erxes.io/docs/core-contacts) | | Catalog, categories, units of measure | `modules/products` | [Products](https://erxes.io/docs/core-products) | | Custom fields, tags, dynamic audiences | `modules/properties`, `modules/tags`, `modules/segments` | [Properties, Tags & Segments](https://erxes.io/docs/core-properties-tags-segments) | | Document templates with placeholders | `modules/documents` (`modules/forms` is a legacy shell; the working forms module is in `frontline_api`. See [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys)) | [Documents](https://erxes.io/docs/core-forms-documents) | | Workflow engine and approval locks | `modules/automations`, `modules/approval`, plus the `services/automations` worker (see [Background Services](https://erxes.io/docs/architecture-background-services)) | [Automations & Approvals](https://erxes.io/docs/core-automations-approvals) | | Campaigns and in-app/email notifications | `modules/broadcast`, `modules/notifications` | [Broadcasts & Notifications](https://erxes.io/docs/core-broadcast-notifications) | | Outbound transports and inbound mail | `src/utils/email` and `erxes-api-shared/src/utils/email`; inbound via `cloudflare/mail-worker` into `frontline_api` | [Email Delivery](https://erxes.io/docs/api-email) | | Bulk CSV load and extraction | `modules/import-export` | [Import & Export](https://erxes.io/docs/core-import-export) | Core API also contains modules without a dedicated page: `clientportal` (portal records and portal-user auth, see [Client Portal](https://erxes.io/docs/app-client-portal)), `apps` (machine tokens), `conformities` and `relations` (record linking), `internalNote`, `search`, `template`, and `logs`. ## Guides - [Organizations, Users & Permissions](https://erxes.io/docs/core-organizations-users): Tenants, team members, structure, and RBAC - [Contacts](https://erxes.io/docs/core-contacts): Customers and companies with tags and segments - [Products](https://erxes.io/docs/core-products): Catalog, categories, units, and pricing helpers - [Properties, Tags & Segments](https://erxes.io/docs/core-properties-tags-segments): Custom fields, labels, and dynamic audiences - [Documents](https://erxes.io/docs/core-forms-documents): Reusable document templates with placeholders, print and barcode rendering - [Automations & Approvals](https://erxes.io/docs/core-automations-approvals): Workflows, executions, and approval locks - [Broadcasts & Notifications](https://erxes.io/docs/core-broadcast-notifications): Campaigns and in-app plus email notifications - [Email Delivery](https://erxes.io/docs/api-email): Transactional email transports, templates, and the Cloudflare mail worker - [Import & Export](https://erxes.io/docs/core-import-export): Bulk-load and extract records --- # Organizations, Users & Permissions Tenants, team members, structure, and RBAC Manage team members, organization structure (branches, departments, units, positions), brands, and the role-based access control that every plugin enforces. For sign-in flows and tokens, see [Authentication](https://erxes.io/docs/api-authentication). For a full plugin permission declaration example, see [Plugin Metadata Extensions](https://erxes.io/docs/plugin-metadata-extensions). ## Module layout Users, structure, brands, and settings all live under `modules/organization/` in core-api. RBAC is a separate `modules/permissions/` module. The `User` Mongoose schema is shared through `erxes-api-shared` so plugins can use the same model. ```text backend/core-api/src/modules/ organization/ routes.ts # /initial-setup, /get-frontend-plugins, # /sso-callback, /ml-callback, /core-login team-member/ graphql/{schema,queries,mutations}.ts db/models/Users.ts # model over the shared user schema trpc/user.ts # users.find, findOne, create, updateOne, # updateMany, setActiveStatus, getCount, # comparePassword, checkLoginAuth meta/activity-log/ # member change, login, invitation events meta/import-export/ # CSV headers, row processors structure/ graphql/schemas/{branch,department,position,units,structure}.ts graphql/resolvers/{queries,mutations,customResolvers}/ db/definitions/structure.ts, db/models/Structure.ts trpc/{branch,department,unit,index}.ts brand/graphql/{schema,queries,mutations}.ts + trpc/brand.ts settings/ # configs + favorites whitelabel/ # orgWhiteLabel definitions/models permissions/ graphql/schemas/permission.ts, resolvers/{queries,mutations} db/definitions/permissions.ts, db/models/Permissions.ts trpc/permission.ts # permissionGroups.find auth/ # login, password reset, magic link, OAuth apps backend/core-api/src/meta/permissions.ts # core's IPermissionConfig ``` Frontend: `frontend/core-ui/src/modules/settings/team-member/`, `settings/structure/`, `settings/permissions/`, `settings/brands/`, plus reusable pickers in `frontend/libs/ui-modules/src/modules/team-members/` and `structure/`. ## Data model `User` ([users.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/users/db/definitions/users.ts)): | Field | Notes | | ----- | ----- | | `email`, `username`, `password` | Login identity; `password` is optional (invitations) | | `isActive`, `isOwner` | Owner bypasses all permission checks | | `details` | `firstName`, `lastName`, `fullName`, `avatar`, `position`, `employeeId`, … | | `permissionGroupIds` | Mix of default group ids (`core:admin`) and custom `PermissionGroup` `_id`s | | `customPermissions` | Per-user overrides outside any group | | `branchIds`, `departmentIds`, `positionIds`, `unitId` | Structure scoping | | `brandIds` | Brand scoping for forms and inbox content | | `customFieldsData`, `links`, `emailSignatures` | Extended profile data | Structure documents (`structure/db/definitions/structure.ts`) share one schema pattern: `title`, `code`, `parentId`, `supervisorId`, `userIds`, `status`. `PermissionGroup` (`permissions/db/definitions/permissions.ts`) holds `name`, `description`, and `permissions[]` of `{ plugin, module, actions, scope }`. ## GraphQL API ### Team members (`team-member/graphql/schema.ts`) | Operation | Key arguments | | --------- | ------------- | | `users` | `searchValue`, `isActive`, `ids`, `brandIds`, `departmentId`, `branchId`, `branchIds`, `departmentIds`, `unitId`, `segment`, `isAssignee`, `status`, `sortField`, `excludeIds`, cursor params; returns `UsersListResponse` | | `allUsers` | `isActive`, `ids`, `assignedToMe`, `searchValue`; unpaginated `[User]` | | `userDetail` | `_id` | | `usersTotalCount` | same selector args as `users` | | `userMovements` | `userId!`, `contentType` | | `usersInvite` | `entries: [InvitationEntry]` (`email`, `password`, `permissionGroupIds`) | | `usersEdit` / `usersEditProfile` | `_id!` plus member fields / `username!`, `email!`, `details`, `links`, `employeeId`, `positionIds` | | `usersResetMemberPassword` | `_id!`, `newPassword!` | | `usersSetActiveStatus` / `usersSetActiveStatusBatch` | `_id!` / `_ids: [String!]!` | | `usersResendInvitation` / `usersConfirmInvitation` | `email!` / `token` | | `usersCreateOwner` | `email!`, `password!`, `firstName!`, `lastName`, `purpose`, `subscribeEmail`; first-account bootstrap | ### Structure (`structure/graphql/schemas/*`) | Operation | Key arguments | | --------- | ------------- | | `branches` / `branchesMain` / `branchDetail` | `searchValue`, `ids`, `withoutUserFilter` / `_id!` | | `departments` / `departmentsMain` / `departmentDetail` | same pattern | | `units` / `unitsMain` / `unitDetail` | `searchValue` / `_id!` | | `positions` / `positionsMain` / `positionDetail` | `searchValue`, `ids` / `_id` | | `structureDetail` | none; returns the single org structure record | | `branchesAdd`, `departmentsAdd`, `unitsAdd`, `positionsAdd`, `structuresAdd` | `title`, `code`, `parentId`, `supervisorId`, `userIds`, `status`, plus type-specific fields | | `*sEdit` / `*sRemove` | `_id!` / `ids: [String!]` | `cp*` variants (`cpBranches`, `cpUnits`, `cpDepartments`, `cpCustomers`, …) expose the same records to client-portal contexts. ### Permissions (`permissions/graphql/schemas/permission.ts`) | Operation | Key arguments | | --------- | ------------- | | `permissionModules` | none; returns every plugin's declared modules and actions | | `permissionDefaultGroups` | none; returns declared `defaultGroups` with resolved members | | `permissionGroups` / `permissionGroupDetail` | none / `id!` | | `currentUserPermissions` | none; returns effective permissions for the caller | | `permissionGroupAdd` / `permissionGroupEdit` | `name!`, `description`, `permissions: [PermissionInput]!` / `_id!` plus the same optional fields | | `permissionGroupRemove` | `_id!` | | `userUpdatePermissionGroups` / `usersUpdatePermissionGroups` | `userId!`, `groupIds: [String]!` / `userIds: [String]!`, `groupIds` | | `userAddCustomPermission` / `userRemoveCustomPermission` | `userId!`, `permission: PermissionInput!` / `userId!`, `module!` | ## How RBAC is enforced `generateApolloContext` ([apollo/utils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/utils.ts)) injects `checkPermission` into every resolver context. A resolver calls it with the action name: ```ts // backend/core-api/src/modules/contacts/graphql/resolvers/mutations/company.ts await checkPermission('contactsCreate'); ``` `checkPermission` is `checkPermissionGroup(subdomain, user)` from [permissions/utils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/permissions/utils.ts). It: 1. Calls `checkLogin`, which rejects anonymous callers with `UNAUTHORIZED`. 2. Runs `canGroup`: owners always pass; otherwise the user's action map is built from their `permissionGroupIds` and `customPermissions`, cached in Redis under `user_actions_`. Group ids containing `:` (such as `core:admin`) are looked up in each plugin's declared `defaultGroups`; other ids are `PermissionGroup` documents fetched over tRPC (`module: 'permissionGroups'`, `action: 'find'`). 3. Runs `checkOAuthScope`: sessions authenticated with an OAuth token must also hold a scope mapped to the action (`oauthScope`/`oauthScopes` in the config, else `-:manage`). Cookie sessions skip this check. `wrapPermission` (login only) and `wrapPublicResolver` (`forClientPortal`, `cpUserRequired`) wrap resolvers that need weaker checks. `clearGroupActionsCache` invalidates the Redis action map when groups or users change. ## Declaring permissions in a plugin Each service exports an `IPermissionConfig` from `src/meta/permissions.ts` and passes it to `startPlugin({ meta: { permissions } })`. This is the Sales plugin's declaration (trimmed): ```ts // backend/plugins/sales_api/src/meta/permissions.ts export const permissions: IPermissionConfig = { plugin: 'sales', modules: [ { name: 'deal', description: 'Deals management', scopeField: null, ownerFields: ['userId', 'assignedUserIds'], scopes: [ { name: 'own', description: 'Deals created by or assigned to user' }, { name: 'group', description: 'Deals in user departments' }, { name: 'all', description: 'All deals' }, ], actions: [ { title: 'View deals', name: 'showDeals', description: 'View deals', always: true }, { title: 'Add deals', name: 'dealsAdd', description: 'Create deals' }, ], }, ], defaultGroups: [ { id: 'sales:admin', name: 'Sales Admin', description: 'Full access to Sales plugin', permissions: [ { plugin: 'sales', module: 'deal', actions: ['showDeals', 'dealsAdd', 'dealsEdit'], scope: 'all' }, ], }, ], }; ``` Default groups are not seeded into MongoDB. They are read live from the plugin registry: `permissionDefaultGroups` reads `config.meta.permissions.defaultGroups` from every active plugin, and `getGroupActionsMap` grants the group's actions to any user whose `permissionGroupIds` contains the group `id`. Actions marked `always: true` are pre-checked in the permission UI; they are not an automatic grant. Core's own declaration lives at [src/meta/permissions.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/meta/permissions.ts): modules `contacts`, `products`, `properties`, `tags`, `segments`, `documents`, `brands`, `organization`, `teamMembers`, `broadcasts`, `importExport`, `bundle`, `permissions`, `apps`, `clientPortal`, `approval`, `automations`, `logs`, `internalNotes`, with a `core:admin` default group. ## Member email activity log The team-member record table's "Activity log" sheet ([TeamMemberActivityLogSheet.tsx](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/modules/settings/team-member/components/TeamMemberActivityLogSheet.tsx), opened from `TeamMemberMoreColumn`) shows the member's outbound email activity: Messages (`EmailDeliveriesRecordTable`) and Addresses (`EmailAddressesRecordTable`) filtered by their email address. It is backed by the `emailDeliveries` and `emailAddresses` queries in the notifications module, not by a separate audit collection. Separately, `team-member/meta/activity-log/` sends member profile changes, logins, and invitations to the shared activity-log pipeline (`userMovements` exposes structure/position moves). ## Example ```graphql query Members($searchValue: String) { users(searchValue: $searchValue, isActive: true, limit: 20) { list { _id email details { fullName position } branchIds permissionGroupIds } totalCount pageInfo { hasNextPage endCursor } } } mutation Invite { usersInvite(entries: [ { email: "dev@example.com", password: "Temp#1234", permissionGroupIds: ["core:admin"] } ]) } ``` --- # Contacts Customers and companies with tags and segments Store customers and companies with tags, segments, custom fields, and links to plugin records. For custom attributes, see [Properties, Tags & Segments](https://erxes.io/docs/core-properties-tags-segments). For bulk loads, see [Import & Export](https://erxes.io/docs/core-import-export). ## Module layout ```text backend/core-api/src/modules/contacts/ graphql/schemas/{customer,company}.ts graphql/resolvers/{queries,mutations,customResolvers}/ db/definitions/{customers,company}.ts db/models/{Customers,Companies}.ts trpc/{customer,company,index}.ts # service-to-service access meta/activity-log/{customers,companies}/ meta/import-export/{import,export}/ # CSV handlers per entity constants.ts, utils.ts, @types/customer.ts ``` Linking modules alongside it: `modules/conformities/` (legacy type-pair links) and `modules/relations/` (generic entity-to-entity edges). Frontend lives in `frontend/core-ui/src/modules/contacts/{customers,companies,client-portal-users}` with shared pickers in `frontend/libs/ui-modules/src/modules/contacts/`. ## Data model `Customer` ([customers.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/contacts/db/definitions/customers.ts)): | Field | Notes | | ----- | ----- | | `state` | `visitor` by default; promoted to `lead`/`customer` | | `firstName`, `lastName`, `middleName`, `primaryEmail`, `emails`, `primaryPhone`, `phones`, `code` | Identity and contact points | | `emailValidationStatus`, `phoneValidationStatus` | `unknown` until verified | | `visitorContactInfo`, `trackedData`, `location` | Visitor/session capture | | `leadStatus`, `status`, `isSubscribed`, `ownerId`, `position`, `department` | Pipeline and ownership | | `tagIds`, `customFieldsData`, `propertiesData`, `links` | Labels and custom data | | `searchText`, `searchTokens` | Maintained by the search token config (below) | `Company` (`db/definitions/company.ts`): `primaryName`, `names`, `code`, `size`, `industry`, `businessType`, `website`, `primaryEmail`, `primaryPhone`, `addresses`, `parentCompanyId`, `score`, `doNotDisturb`, `mergedIds`, plus the same `tagIds`, `customFieldsData`, `propertiesData`, `trackedData`, `ownerId`, `status` pattern. ## Search tokens `customerSearchTokenConfig` in `db/definitions/customers.ts` controls partial-match search. Each entry names a `path`, a `mode` (`prefix`, `word-prefix`, `exact`), and a `minLength`: | Path | Mode | Min length | | ---- | ---- | ---------- | | `firstName`, `middleName`, `lastName` | `prefix` | 2 | | `primaryEmail`, `emails` | `word-prefix` | 3 | | `primaryPhone`, `phones` | `word-prefix` | 1 | | `code` | `exact` | 1 | | `visitorContactInfo.email` | `word-prefix` | 3 | | `visitorContactInfo.phone` | `word-prefix` | 1 | Tokens are materialized into `searchTokens` (indexed with `state` and `status`), so `searchValue` filters stay MongoDB queries. Inputs shorter than `minLength` do not match, so a too-short search can look like it finds nothing. ## GraphQL API ### Customers (`graphql/schemas/customer.ts`) | Operation | Key arguments | | --------- | ------------- | | `customers` | `searchValue`, `type`, `ids`, `excludeIds`, `tagIds`, `excludeTagIds`, `tagWithRelated`, `segment`, `segmentIds`, `brandIds`, `integrationIds`, `integrationTypes`, `clientPortalId`, `formIds`, `leadStatus`, `status`, `sex`, `birthDate`, `dateFilters`, `propertiesData`, `emailValidationStatus`, `sortField`, `sortDirection`, `autoCompletion`, `autoCompletionType`, `startDate`, `endDate`, conformity args, cursor params; returns `CustomersListResponse` | | `customerDetail` | `_id!` | | `customersCount` | `types: [CUSTOMER_RELATION_TYPE]` | | `contactsLogs` | `action`, `content`, `contentType` | | `cpCustomers` / `cpCustomerDetail` | client-portal variants | | `customersAdd` / `customersEdit` | `state`, `firstName`, `lastName`, `primaryEmail`, `emails`, `primaryPhone`, `phones`, `primaryAddress`, `addresses`, `ownerId`, `position`, `department`, `leadStatus`, `description`, `links`, `code`, `sex`, `birthDate`, `avatar`, `isSubscribed`, `propertiesData`, validation statuses | | `customersMerge` | `customerIds`, `customerFields` | | `customersRemove` | `customerIds` | | `customersChangeState` / `customersChangeStateBulk` | `_id!`, `value!` / `_ids: [String!]!`, `value!` | | `customersVerify`, `customersChangeVerificationStatus` | `verificationType!` / `customerIds`, `type!`, `status!` | ### Companies (`graphql/schemas/company.ts`) | Operation | Key arguments | | --------- | ------------- | | `companies` | same selector family as `customers`; returns `CompaniesListResponse` | | `companyDetail` | `_id!` | | `companiesAdd` / `companiesEdit` | `primaryName`, `names`, `avatar`, `size`, `industry`, `businessType`, `website`, `primaryEmail`, `emails`, `primaryPhone`, `phones`, `primaryAddress`, `addresses`, `parentCompanyId`, `code`, `location`, `ownerId`, `tagIds`, `propertiesData`, `links`, `isSubscribed` | | `companiesMerge` | `companyIds`, `companyFields` | | `companiesRemove` | `companyIds` | ### Linking records - **Conformities** (`modules/conformities`): `conformityAdd`, `conformityEdit` mutations store main-type/rel-type pairs. Filter contacts with `conformityMainType`, `conformityMainTypeId`, `conformityRelType`, `conformityIsRelated`, `conformityIsSaved` on the list queries. - **Relations** ([modules/relations](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/relations/graphql/schema.ts)): generic `entities: [{ contentType, contentId }]` edges. `createRelation`, `manageRelations`, `getRelationsByEntity` let plugins link contacts to deals, tickets, or their own types without touching the contact document. List queries return a cursor envelope `{ list, pageInfo { hasNextPage, endCursor }, totalCount }`, controlled by `limit`, `cursor`, `cursorMode`, `direction`, `orderBy`. ## Example ```graphql query CustomersByTag($tagIds: [String], $cursor: String) { customers(tagIds: $tagIds, cursor: $cursor, limit: 20) { list { _id firstName primaryEmail tagIds customFieldsData cursor } totalCount pageInfo { hasNextPage endCursor } } } mutation AddCustomer { customersAdd( firstName: "Ari" primaryEmail: "ari@example.com" primaryPhone: "+15551234567" code: "CUST-001" ) { _id state } } ``` ## tRPC access Other services reach contacts through `sendTRPCMessage({ pluginName: 'core', module: 'customers', action: ... })`. Procedures on [trpc/customer.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/contacts/trpc/customer.ts) include `find`, `findOne`, `findActiveCustomers`, `getCustomerName`, `getWidgetCustomer`, `count`, `createCustomer`, `updateCustomer`, `updateOne`, `updateMany`, `removeCustomers`, `markCustomerAsActive`, `createMessengerCustomer`, `updateMessengerCustomer`, `saveVisitorContactInfo`, `updateLocation`, `updateSession`, `setUnsubscribed`, `createOrUpdate`, `tag`. The company router mirrors the core CRUD set. ## Permissions Mutations check core actions from `meta/permissions.ts`: `contactsCreate`, `contactsUpdate`, `contactsDelete`, `contactsMerge`, `customersImportManage`, `customersExportManage`, `companiesImportManage`, `companiesExportManage`. `contactsRead` is marked `always: true` and supports scopes `own` (records in `createdBy`/`ownerId`) and `all`. --- # Products Catalog, categories, units, and pricing helpers Maintain the shared catalog of products and services, categories, units of measure, bundles, and pricing rules. For POS and deal flows that use the catalog, see [Sales](https://erxes.io/docs/plugin-sales) and [POS Client](https://erxes.io/docs/app-pos-client). ## Module layout ```text backend/core-api/src/modules/products/ graphql/schemas/{product,category,uom,package,rule,similarity,config,index}.ts graphql/resolvers/{queries,mutations,customResolvers}/ db/definitions/{products,categories,uoms,packages,rules,similarities,configs}.ts db/models/{Products,Categories,Uoms,Packages,Rules,Similarities,Configs}.ts trpc/{product,category,uom,similarity,config,index}.ts meta/automations.ts # AI knowledge provider for automations meta/document/productReplacer.ts meta/import-export/{import,export}/ meta/template/ # category templates utils/productRule.ts ``` Frontend: `frontend/core-ui/src/modules/products/` (record table, categories, UoM, packages) with shared product pickers in `frontend/libs/ui-modules/src/modules/products/`. ## Data model `Product` ([products.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/products/db/definitions/products.ts)): | Field | Notes | | ----- | ----- | | `name`, `shortName`, `code` | `code` is unique; the stable external key | | `categoryId` | Links to `ProductCategory` (tree via `parentId`, `order`) | | `type` | `product`, `service`, `unique`, `subscription` | | `status` | `active` or `deleted` (soft delete) | | `unitPrice`, `currency` | Base price; `discounts[]` and `inventories` hold extras | | `barcodes`, `variants`, `barcodeDescription`, `sameMasks`, `sameDefault` | Barcode/variant matching for POS | | `uom`, `subUoms` | Unit of measure and convertible sub-units | | `vendorId`, `scopeBrandIds`, `tagIds`, `mergedIds` | Ownership and dedup | | `customFieldsData`, `propertiesData` | Custom properties | | `attachment`, `attachmentMore`, `videos`, `pdfAttachment` | Media | | `duration`, `durationType` | For service-type items | | `similarityId` | Links to `Similarity` groups | `Uom` (`db/definitions/uoms.ts`): `name`, `code` (unique), `isForSubscription`, `subscriptionConfig` (`period`, `rule`, `specificDay`, `subsRenewable`). `ProductRule` prices products by conditions; `ProductPackage` bundles items; `Similarity` groups mask-matched products; `ProductsConfig` stores module-wide settings (`productsConfigsUpdate`). ## GraphQL API ### Products ([graphql/schemas/product.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/products/graphql/schemas/product.ts)) | Operation | Key arguments | | --------- | ------------- | | `productsMain` | `type`, `status`, `categoryId`, `categoryIds`, `searchValue`, `vendorId`, `brandIds`, `tag`, `tagIds`, `excludeTagIds`, `tagWithRelated`, `ids`, `excludeIds`, `pipelineId`, `boardId`, `segment`, `segmentIds`, `propertiesData`, `groupedSimilarity`, `similarity`, `image`, `brand`, `branchId`, `departmentId`, `minRemainder`/`maxRemainder`, `minPrice`/`maxPrice`, discount range args, `discountConditions`, `sortField`, `sortDirection`, cursor params; returns `ProductsListResponse` | | `products` | same filters with `page`/`perPage`; returns a plain `[Product]` | | `productsTotalCount` | same filters | | `productDetail` | `_id` | | `productSimilarities` | `_id!`, `groupedSimilarity` | | `productCountByTags` | none | | `productsAdd` / `productsEdit` | `name`, `shortName`, `categoryId`, `type`, `description`, `barcodes`, `variants`, `barcodeDescription`, `unitPrice`, `code`, `propertiesData`, `attachment`, `attachmentMore`, `videos`, `vendorId`, `scopeBrandIds`, `uom`, `subUoms`, `currency`, `duration`, `durationType`, `pdfAttachment` | | `productsMerge` | `productIds`, `productFields` | | `productsDuplicate` / `productsRemove` | `_id!` / `productIds: [String!]` | ### Categories, units, packages, rules | Operation | Key arguments | | --------- | ------------- | | `productCategories` / `productCategoriesTotalCount` / `productCategoryDetail` | `parentId`, `searchValue`, `status`, `ids` / `_id` | | `categoriesWithChilds` | `ids: [String!]!` | | `productCategoriesAdd` / `productCategoriesEdit` / `productCategoriesRemove` | `name`, `code`, `parentId`, `description`, `meta`, `attachment`, `status` / `_id!` | | `uoms` / `uomsTotalCount` | none | | `uomsAdd` / `uomsEdit` / `uomsRemove` | `name`, `code`, `isForSubscription`, `subscriptionConfig`, `timely` / `uomIds: [String!]` | | `productPackages` / `productPackageDetail` | cursor params / `_id!` | | `productPackagesAdd` / `productPackagesEdit` / `productPackagesChangeStatus` / `productPackagesRemove` | package fields / `_ids`, `status!` | | `productRules` / `productRulesWithCount` | none | | `productRulesAdd` / `productRulesEdit` / `productRulesRemove` | rule fields / `_ids` | | `productBulkSimilarity` / `productBulkSimilarities` | `_id!` / `page`, `perPage`, `searchValue` | | `productsConfigs` / `productsConfigsUpdate` | none / `configsMap: JSON!` | ## Example ```graphql query ProductList($categoryId: String) { productsMain(categoryId: $categoryId, status: "active", limit: 20) { list { _id name code unitPrice uom cursor } totalCount pageInfo { hasNextPage endCursor } } } mutation AddProduct { productsAdd( name: "Espresso blend 1kg" code: "COF-ESP-1KG" type: "product" unitPrice: 18.5 currency: "USD" uom: "bag" barcodes: ["4006381333931"] ) { _id code } } ``` ## How plugins use products Services do not read the `Products` collection directly. They call the tRPC router in [trpc/product.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/products/trpc/product.ts) through `sendTRPCMessage({ pluginName: 'core', module: 'products', action: ... })`. Procedures: `find` (with automatic `categoryId` expansion to child categories), `findOne`, `createProduct`, `updateProduct`, `updateProducts`, `removeProducts`, `count`. Example from the POS order route in `sales_api`: ```ts // backend/plugins/sales_api/src/modules/pos/routes.ts const products = await sendTRPCMessage({ subdomain, pluginName: 'core', module: 'products', action: 'find', input: { query: { status: { $ne: 'deleted' }, categoryId: { $in: categoryIds } } }, defaultValue: [], }); ``` The same pattern appears in the sales pipeline product picker and ecommerce module. Products also show up through `meta/document/productReplacer.ts` (document templates), `meta/automations.ts` (AI knowledge provider), and the segment content type `core:products.products`. ## Permissions Core permission module `products` (`meta/permissions.ts`): `productsRead` (`always: true`), `productsCreate`, `productsUpdate`, `productsDelete`, `productsMerge`, `productCategoriesManage`, `productsConfigsManage`, `uomsManage`, `productRulesManage`, `productsImportManage`, `productsExportManage`. Scope is `all` only; there is no per-owner product scope. --- # Properties, Tags & Segments Custom fields, labels, and dynamic audiences Model custom fields and system-field visibility, label records with tags, and build dynamic audiences with segments. For bulk changes, see [Import & Export](https://erxes.io/docs/core-import-export). ## Properties `backend/core-api/src/modules/properties/`: ```text db/definitions/{common,field,group,systemField}.ts db/models/{Field,Group,SystemField}.ts graphql/schemas/{field,group,property,systemField,index}.ts graphql/resolvers/{queries,mutations,customResolvers}/ trpc/{field,group,index}.ts ``` A `Field` ([field.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/properties/db/definitions/field.ts)) defines `name`, `code`, `groupId`, `contentType`, `contentTypeId`, `type`, `order`, `options[]`, `logics`, `validations`, `configs`, `icon`, `isVisible`, `isVisibleToCreate`, `isRequired`, `isVisibleInCard`. A `FieldGroup` orders fields per content type. Values written to records land in `customFieldsData`/`propertiesData` as structured JSON. | Operation | Key arguments | | --------- | ------------- | | `fields` | `params: { contentType, contentTypeId, groupId, cursor params }`; returns `FieldListResponse` | | `fieldDetail` | `_id!` | | `fieldAdd` / `fieldEdit` / `fieldRemove` | `name`, `code`, `groupId`, `contentType`, `contentTypeId`, `type`, `options`, `validations`, `logics`, `configs`, `icon`, `isVisible`, `isVisibleToCreate`, `isRequired`, `isVisibleInCard`, `order` | | `fieldGroups` | `params: { contentType, contentTypeId, cursor params }` | | `fieldGroupAdd` / `fieldGroupEdit` / `fieldGroupRemove` | group fields / `_id!` | | `fieldGroupsUpdateOrder` | `orders: [FieldGroupOrderItem!]!` | | `propertyTypes` | none; returns every registered content type | | `propertySystemFields` | `contentType!` | | `propertySystemFieldEdit` | `contentType!`, `code!`, `isVisible`, `isVisibleToCreate`, `isRequired`, `logics` | | `cpFields`, `cpFieldDetail`, `cpFieldGroups` | client-portal variants | ### System fields ("Basic information") Each plugin can declare built-in `systemFields` inside `meta/properties.ts` `types[]` (core declares them for `customer`, `company`, `product`, `user`; for example customer `firstName`, `primaryEmail`, `ownerId`, `tagIds`). `propertySystemFields` merges the declaration with tenant overrides stored in the `SystemFieldSettings` collection ([db/models/SystemField.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/properties/db/models/SystemField.ts)), and `propertySystemFieldEdit` persists `isVisible`, `isVisibleToCreate`, `isRequired`, and `logics` (operators `is`/`isNot`, actions `show`/`hide`). Settings → Properties renders them as the "Basic information" section (`frontend/core-ui/src/modules/properties/components/record/PropertiesSystemFieldsSection.tsx`). ## Tags `backend/core-api/src/modules/tags/` (`graphql/schemas.ts`, `queries.ts`, `mutations.ts`, `db/definitions/tags.ts`, `db/models/Tags.ts`, `taggable.ts`, `trpc/tag.ts`). A `Tag` has `name` (unique), `colorCode`, `parentId`, `relatedIds`, `isGroup`, `type` (the content type it applies to), `description`, `objectCount`, `order`. `taggable.ts` looks up the target model from the tag's `type` so any entity can be tagged. | Operation | Key arguments | | --------- | ------------- | | `tagsGetTypes` | none; returns `meta.tags.types` from every plugin | | `tags` | `type`, `searchValue`, `parentId`, `ids`, `excludeIds`, `isGroup`, `instanceId`, `includeWorkspaceTags`, `sortField`, cursor params; returns `TagsListResponse` | | `tagsMain` / `tagDetail` / `tagsQueryCount` | `type`, `excludeWorkspaceTags` / `_id!` / `type`, `searchValue` | | `tagsAdd` / `tagsEdit` | `name!` plus `type`, `colorCode`, `parentId`, `isGroup`, `description` | | `tagsTag` | `type!`, `targetIds: [String!]!`, `tagIds: [String!]!` | | `tagsRemove` | `_id!` | | `cpTags`, `cpTagsAdd`, `cpTagsTag` | client-portal variants | Tag types are declared per plugin in `meta/tags.ts` (`tags.types`; core registers `customer`, `company`, `product`, `user`, `form`, `automation`, `documents`). A tag only applies to records whose content type matches its `type`; `tagsGetTypes` shows the registry. Use tags for operator-driven labels, not for access control. ## Segments `backend/core-api/src/modules/segments/`: ```text graphql/schemas/index.ts, resolvers/{queries,mutations,customResolvers} db/definitions/{segments,segmentNodes,segmentHistory}.ts db/models/{Segments,SegmentHistory}.ts trpc/{index,segments}.ts utils/{access,publishBuild,relationEdges,runSegment,segmentGateway}.ts ``` A `Segment` ([segments.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/segments/db/definitions/segments.ts)) has `contentType`, `name`, `ownedBy`, `description`, `color`, `root` (the condition tree), `dependsOn`, `fingerprint`, `visibility` (`private`/`organization`), `ownerId`, `status` (`draft`/`building`/`active`/`failed`/`cancelled`), `revision`, `membersCount`, `membersCountedAt`, `timeSensitive`, and `build*` progress fields (`buildStartedAt`, `buildProcessed`, `buildTotal`, `buildCancelRequested`). `SegmentHistory` stores per-day membership (`count`, `joined`, `left`) for `segmentGrowth`. | Operation | Key arguments | | --------- | ------------- | | `segmentsGetTypes` | none; returns registered content types | | `segments` / `segmentDetail` | `contentTypes: [String]!`, `ids`, `excludeIds`, `searchValue` / `_id!` | | `segmentFields` | `contentType!`; returns filterable fields incl. custom properties | | `segmentRelations` | `subjectType!`; returns relations the segment can traverse | | `segmentsPreviewCount` | `contentType!`, `root!`; live count for the form | | `segmentSameDefinition` | `contentType!`, `root!`, `excludeId`; duplicate detection | | `segmentMembers` / `segmentMemberCount` | `segmentId!`, `cursor`, `limit` | | `segmentGrowth` | `segmentId!`, `days` | | `segmentUsage` | `ids: [String!]!`; returns automations and segments referencing it | | `segmentsAdd` / `segmentsEdit` | `contentType!`, `name`, `description`, `color`, `root!`, `visibility`, `status`, `ownedBy` | | `segmentsRemove` | `ids: [String!]!` | | `segmentsRebuild` / `segmentsStopRebuild` | `_id!` | ### How evaluation runs Evaluation is **MongoDB**, not Elasticsearch. `erxes-api-shared/src/core-modules/segments/` translates the `root` node tree into a `SegmentMongoFilter` (`mongoFilter.ts`, `evaluate.ts`, `memberQuery.ts`, `evaluateBatch.ts`, `measureRelations.ts`). The segmentation worker runs in the **logs service** ([segmentWorker.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/services/logs/src/bullmq/segments/segmentWorker.ts)) with four BullMQ queues (`changed`, `forget`, `rebuild`, `reconcile`), each with a `SEGMENT_CONCURRENCY_*` env override. Membership changes are applied per plugin through `applyMembership` producers and logged into `SegmentHistory`. > [!WARNING] > **Segments depend on the logs service** > > A segment stuck in `building` usually means the logs service is not running; check `SEGMENT_CONCURRENCY_REBUILD` and the service logs (`segmentsStopRebuild` cancels a build). If a count comes back lower than expected, `SegmentMemberCount.unsupported` lists tree nodes the Mongo filter could not express and `exceeded` means evaluation bailed early. Plugins register segment content types through `meta/segments` passed to `startPlugin` (or `initSegmentProducers` for core): `contentTypes`, `segmentFields`, `segmentFieldNamespaces`, `segmentRelations`, plus `evaluateFields`, `listSegmentMembers`, `countSegmentMembers`, `applyMembership` handlers. Core registers `core:organization.users`, `core:contacts.companies`, `core:contacts.customers`, `core:contacts.leads`, `core:products.products` in `meta/segments/contentTypes.ts`; sales registers its deal/pos types the same way. ## Permissions From `meta/permissions.ts`: `fieldsManage`, `fieldGroupsManage` (properties, read is `always: true`); `tagsCreate`, `tagsUpdate`, `tagsDelete`, `tagsTag` (`tagsRead` always); `segmentsManage` (`segmentsRead` always). All use scope `all`. --- # Documents Reusable document templates with placeholders, print and barcode rendering Render reusable document templates with placeholders, print, and barcode output. > [!NOTE] > **Looking for forms?** > > The working forms module is `backend/plugins/frontline_api/src/modules/form/`; see [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys). Core keeps only a legacy shell in `modules/forms/` (the `fieldsCombinedByContentType` query plus `fields`/`fieldsGroups` helpers used by documents and properties). ## The documents module `backend/core-api/src/modules/documents/`: ```text graphql/schema.ts, queries.ts, mutations.ts db/definitions/documents.ts, db/models/Documents.ts customResolvers/document.ts trpc/document.ts # documents.find, findOne, print routes.ts # GET /print blocksToHtml.ts, barcode.ts, utils.ts ``` A `Document` ([documents.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/documents/db/definitions/documents.ts)) has `contentType` (`:` like `core:product`), `subType`, `name`, `code`, `content` (editor blocks), `replacer` (placeholder map), `tagIds`, `createdUserId`. The GraphQL type also resolves `createdUser` and `approvalLockState`. | Operation | Key arguments | | --------- | ------------- | | `documents` | `contentType`, `subType`, `searchValue`, `dateFilters`, `userIds`, `tagIds`, `limit`, cursor params; returns `DocumentListResponse` | | `documentsDetail` | `_id!` | | `documentsTypes` | none; returns types across every plugin's `meta.documents` | | `documentsGetEditorAttributes` | `contentType!`; returns the placeholder list for the editor | | `documentsTotalCount` | `searchValue`, `contentType` | | `documentsProcess` | `_id`, `replacerIds`, `config`; renders the template, returns an HTML string | | `documentsSave` | `_id`, `contentType`, `subType`, `name!`, `content`, `replacer`, `code`; upsert | | `documentsRemove` | `_id!` | `blocksToHtml` and `barcode` render printable output; `routes.ts` serves `GET /print` for browser printing and download flows. ### How a plugin contributes document types A plugin contributes three pieces, and core dispatches on the `contentType` prefix (`:`): - `meta/documents.ts`: `documents.types` entries `{ label, contentType, subTypes? }`. `documentsTypes` aggregates them from every active plugin. Sales declares `{ label: 'Sales', contentType: 'sales:deal' }`. - A `documents.editorAttributes` tRPC procedure: `documentsGetEditorAttributes` calls `sendTRPCMessage({ pluginName, module: 'documents', action: 'editorAttributes' })` for non-core types. Sales implements it in `modules/sales/trpc/document.ts`, returning deal attributes like `productsInfo`, `totalAmount`, `stageName`, `assignedUsers`, plus schema fields and `customFieldsData.` entries. - A `.replaceContent` tRPC procedure: `documentsProcess` routes to `module: , action: 'replaceContent'` for non-core types (sales exposes it on `modules/sales/trpc/deal.ts`). The procedure receives `replacerIds`, `content`, `config`, and `contentType`, and returns rendered blocks; `config.dateFormat` controls date rendering (core defaults to `YYYY-MM-DD`). Core's own declaration ([src/meta/documents.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/meta/documents.ts)) registers `core:contact.customer`, `core:contact.company`, `core:product`, `core:user`, `core:broadcast`, keeps `editorAttributes`/`replaceContent` as direct functions in the meta object, and builds product barcodes through `meta/document/productReplacer.ts`. ## Permissions `manageDocuments` (create/edit), `removeDocuments` (delete); `documentsRead` is `always: true`. Owner scope `own` tracks `createdUserId`. ## Example ```graphql mutation SaveTemplate { documentsSave( contentType: "core:product" name: "Product sheet" code: "PROD-SHEET" content: "

{{ name }} — {{ code }} — {{ unitPrice }}

" ) { _id } } query Render($id: String) { documentsProcess(_id: $id, replacerIds: [""], config: { dateFormat: "YYYY-MM-DD" }) } ``` --- # Automations & Approvals Workflows, executions, and approval locks Run event-driven workflows with triggers and actions, and gate sensitive changes behind approval locks. For the worker process itself, see [Background Services](https://erxes.io/docs/architecture-background-services). ## Automations Two halves: the API module stores definitions and history, the `automations-service` executes them. ```text backend/core-api/src/modules/automations/ graphql/schema/{index,queries,mutations,types}.ts, typeDef.ts graphql/resolvers/{queries,mutations,customResolver} db/models/{Automations,Executions,AutomationEmailTemplates, AutomationWorkflowTemplates}.ts db/definitions/{automationEmailTemplate,automationWorkflowTemplate}.ts trpc/{automations,clients,index}.ts backend/core-api/src/meta/automations/ # core triggers/actions via startAutomations backend/services/automations/src/ bullmq/{initMQWorkers,triggerWorker,actionHandlerWorker,aiWorker}.ts executions/actions/{webhook,emailAction}/ mongo/waitingActionsToExecute.ts ai/ # aiAgent, aiAction, provider bridge, knowledge trpc/, constants.ts, main.ts # default port 3302 ``` An `Automation` ([types.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/automations/graphql/schema/types.ts)) holds `name`, `status`, `edgeType`, `flowDirection`, `tagIds`, `triggers: [Trigger]`, `actions: [Action]`, `workflows: [Workflow]`, `duplicatedFrom`, and resolves `createdUser`, `updatedUser`, and `approvalLockState(action)`. Each run is an `Execution` record (`db/models/Executions.ts`) exposed through `automationHistories`. ### GraphQL API | Operation | Key arguments | | --------- | ------------- | | `automationsMain` / `automations` | `page`, `perPage`, `ids`, `excludeIds`, `searchValue`, `sortField`, `sortDirection`, `status`, `tagIds`, `createdByIds`, `updatedByIds`, date ranges, `triggerTypes`, `actionTypes`, cursor params | | `automationDetail` / `cpAutomationDetail` | `_id!` | | `automationHistories` / `automationHistoriesTotalCount` | `automationId!`, `status`, `triggerId`, `triggerType`, `targetId`, `targetIds`, `parentExecutionId`, `failedActionIds`, `errorCodes`, `waitingActionIds`, `beginDate`, `endDate`, cursor params | | `getAutomationExecutionDetail` | `executionId!` | | `automationStats` / `automationExecutionCounts` | `automationId!`, `beginDate`, `endDate` / `automationIds: [String!]!` | | `automationConstants` | none; returns the merged trigger/action registry | | `automationSetPropertyTargets` / `automationNodeOutput` / `automationReferenceFields` | `sourceType!` / `nodeType!` / `type!`, `field!` | | `automationNotes` | `automationId!`, `triggerId`, `actionId` | | `automationsAiAgents` / `automationsAiAgentDetail` / `automationsAiAgentHealth` | `kind` / `_id` / `agentId!` | | `getAutomationWebhookEndpoint` | `_id!`, `waitEventActionId` | | `automationEmailTemplates` / `automationWorkflowTemplates` | `page`, `perPage`, `searchValue` | | `automationsAdd` / `automationsEdit` | `name`, `status`, `edgeType`, `flowDirection`, `triggers: [TriggerInput]`, `actions: [ActionInput]`, `workflows: [WorkflowInput]` (`automationsEdit` takes `_id`, `acknowledgeDuplicate`) | | `automationsDuplicate` / `automationsRemove` / `archiveAutomations` | `_id!`, `name` / `automationIds` / `automationIds`, `isRestore` | | `automationsSaveAsTemplate` / `automationsCreateFromTemplate` | `_id!`, `name`, `duplicate` / `_id` | | `automationsAddNote` / `automationsEditNote` / `automationsRemoveNote` | `automationId`, `triggerId`, `actionId`, `description` | | `automationsAiAgentAdd` / `Edit` / `Remove` / `Reindex` | `name`, `description`, `connection`, `runtime`, `context` / `_id!`, `fileId` | | `automationEmailTemplates*` / `automationWorkflowTemplates*` | `name!`, `description`, `content!` / `entryActionId`, `actions`, `inputs` | ### How plugins register triggers and actions A plugin passes an `AutomationConfigs` object (`meta/automations.ts` → `startPlugin`); core calls `startAutomations(app, 'core', config)` from `initAutomation` in `meta/automations/automations.ts`. The config declares: - `constants.triggers` / `constants.actions`: the catalog `automationConstants` returns. Sales merges `salesAutomationContants` and `posAutomationConstants`. - `receiveActions`: executes the plugin's actions during a run. - `checkCustomTrigger`: evaluates plugin trigger conditions. - `setProperties`, `findObject`: backs `setProperty` actions and target lookups (`findObjectTargets`, `setPropertyTargets`). - `generateAiContext`, `loadAiKnowledgeDocumentBatch`, `lookupAiTool`: optional AI-agent hooks. Handlers are typically composed with `createCoreModuleProducerHandler`, which dispatches by `moduleName` inside the plugin (sales routes `sales` vs `pos`). > [!WARNING] > **A plugin trigger is missing from the builder** > > The plugin's `meta/automations.ts` `constants` must be registered and the plugin enabled. `automationConstants` shows what the gateway aggregated. ### Execution path [initMQWorkers](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/services/automations/src/bullmq/initMQWorkers.ts) in the service starts three BullMQ workers (`trigger`, `action`, `aiAgent`) on the `automations` queue prefix. The API alone only stores definitions and history: with the service stopped, triggers queue but nothing executes. Waiting steps (`delay` actions, `waitEvent` webhook waits) persist in `waitingActionsToExecute` and resume via `waitingActionIds` history filters; `getAutomationWebhookEndpoint` exposes the inbound webhook URL. Built-in action types in `constants.ts`: `delay`, `if`, `setProperty`, `sendEmail`. ## Approvals ```text backend/core-api/src/modules/approval/ graphql/schema/{index,types,queries,mutations}.ts graphql/resolvers/{queries,mutations,customResolvers}.ts db/definitions/{approvalLocks,approvalRequests}.ts db/models/{ApprovalLocks,ApprovalRequests}.ts trpc/approval.ts backend/erxes-api-shared/src/core-modules/approval/ checkApprovalLock.ts, types.ts, zodSchemas.ts, constants.ts ``` An `ApprovalLock` records `contentType`, `contentId`, `lockedBy`, `ownerIdSnapshot`, `allowedUserIds`, `approverScope`, `approvalMode`, `status`, `releasedAt`/`releasedBy`/`releaseReason`. An `ApprovalRequest` tracks `lockId`, `requesterId`, `reason`, `status`, `requiredApproverIds`, per-user `decisions[]` (`userId`, `decision`, `reason`, `at`), `notificationIds`, `resolvedAt`. | Operation | Key arguments | | --------- | ------------- | | `approvalLockState` | `contentType!`, `contentId!`, `ownerId`, `action`; returns `locked`, `hasAccess`, `reason`, `lock`, `pendingRequest` | | `approvalLockStates` | `contentType!`, `contentIds: [String!]!`, `ownerIdsByContentId`, `action` | | `approvalRequests` | `status`, `contentType`, `requesterIds`, `approverIds`, cursor params | | `approvalRequestDetail` | `_id!` | | `approvalLockCreate` | `input: { contentType!, contentTypeId!, ownerId!, allowedUserIds, scope, mode }` | | `approvalLockRelease` / `approvalLockForceRelease` | `_id!` / `_id!`, `reason!` | | `approvalRequestCreate` | `input: { contentType!, contentId!, reason }` | | `approvalRequestApprove` / `Reject` / `Cancel` | `_id!` / `_id!`, `reason` / `_id!` | Plugins do not query the collections. They call `checkApprovalLock.state({ subdomain, contentType, contentId, action, userId })` from [erxes-api-shared](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/approval/checkApprovalLock.ts), which reaches core over tRPC (`module: 'approval'`, `action: 'state'`). If the check is unreachable it fails closed (`locked: true`, `hasAccess: false`, reason "Approval lock check unavailable"). The `Document` and `Automation` GraphQL types expose `approvalLockState(action)` so the UI can render lock state inline. > [!WARNING] > **Locked for everyone** > > If `approvalLockState` reports `locked` for every user, the tRPC call to core failed: the lock check fails closed. Check core-api health before assuming a real lock exists. Approvals are a separate feature, not an automation step type: automations pause on `delay`/`waitEvent` actions, while gated edits go through `approvalLockCreate` → `approvalRequestCreate` → approve/reject → `approvalLockRelease`. ## Permissions `automationsRead` (`always`), `automationsCreate`, `automationsUpdate`, `automationsDelete`, `automationsAiAgentAdd`, `automationsAiAgentEdit`, `automationsAiAgentRemove`; scope `own` (records in `createdBy`) or `all`. `approvalLocksManage` is `always: true`; `approvalLocksForceRelease` is restricted. ## Example ```graphql query ExecutionHistory($automationId: String!) { automationHistories(automationId: $automationId, status: "error", limit: 20) { list { _id status createdAt } totalCount } } mutation RequestApproval { approvalRequestCreate(input: { contentType: "sales:deal" contentId: "" reason: "Discount above threshold" }) { _id status } } ``` --- # Broadcasts & Notifications Campaigns and in-app plus email notifications Send engage campaigns over email, messenger, and SMS, and deliver in-app and email notifications to team members. For inbound mail, DKIM, and the mail worker, see [Email Delivery](https://erxes.io/docs/api-email). ## Broadcasts `backend/core-api/src/modules/broadcast/`: ```text graphql/schemas/{index,engage}.ts graphql/resolvers/{queries,mutations,customResolvers}/ db/definitions/{engages,deliveryReports,broadcastTraces,smsRequest,log,common}.ts db/models/{Engages,DeliveryReports,BroadcastTraces,SmsRequests,Logs,factories}.ts worker/{broadcast,email,messenger,notification,index}.ts trackers/{engage,sendgrid,index}.ts routes/{index,telnyx,tracker,unsubscribe}.ts utils/{broadcast,email,engage,outboundEmail,sender,targeting, telnyx,transporter,worker,widget,...}.ts trpc/{broadcast,index}.ts ``` An `EngageMessage` ([engages.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/broadcast/db/definitions/engages.ts)) carries `kind` (`auto`, `visitorAuto`, `manual`), `method` (`messenger`, `email`, `notification`), `title`, `fromEmail`, `fromUserId`, `cpId`, `targetType`/`targetIds`/`targetCount`, `isDraft`, `isLive`, channel payloads (`email`, `messenger`, `notification`), plus send progress (`totalBatches`, `processedBatches`, `successCount`, `failureCount`, `lastUpdated`) and `status` (`sending`, `completed`, `failed`). ### GraphQL API | Operation | Key arguments | | --------- | ------------- | | `engageMessages` | `kind`, `status`, `method`, `brandId`, `fromUserId`, `searchValue`, cursor params; returns `EngageMessageListResponse` | | `engageMessageDetail` / `engageMessagesTotalCount` | `_id` / same filters | | `engageMessageCounts` | `name!`, `kind`, `status` | | `engageMembers` | `isVerified`, cursor params | | `engageReportsList` | `page`, `perPage`, `customerId`, `status`, `searchValue`; returns `EngageDeliveryReport` | | `engageSmsDeliveries` | `type!`, `to`, `page`, `perPage` | | `engageBroadcastTraces` | `engageMessageId!` | | `engageEmailPercentages`, `engageVerifiedEmails`, `engagesConfigDetail`, `emailSenderOptions` | `scope` for the last | | `engageMessageAdd` / `engageMessageEdit` | `title`, `kind`, `method`, `fromEmail`, `fromUserId`, `cpId`, `targetType`, `targetIds`, `targetCount`, `isDraft`, `isLive`, `email`, `messenger`, `notification` | | `engageMessageSetLive` / `SetPause` / `SetLiveManual` | `_id!` | | `engageMessageRemove` / `engageMessageCopy` | `_ids` / `_id!` | | `engageMessageVerifyEmail` / `engageMessageRemoveVerifiedEmail` | `email!`, `name`, `replyTo`, `scope` / `email!`, `scope` | | `engageMessageSendTestEmail` | `from!`, `to!`, `content!`, `title!` | | `engagesUpdateConfigs` / `broadcastUpdateConfigs` | `configsMap: JSON!` | | `engageSendMail` | `integrationId`, `conversationId`, `subject!`, `body`, `to: [String]!`, `cc`, `bcc`, `from!` | ### Delivery pipeline Sending runs through `worker/` batch processors (`broadcast`, `email`, `messenger`, `notification`); a broadcast is not transactional: create it, set it live, then track per-recipient state in `DeliveryReports`, `BroadcastTraces`, `SmsRequests`, and `Logs`. Open/click tracking lands via `routes/tracker.ts` and `trackers/`; Telnyx webhooks arrive on `routes/telnyx.ts`; unsubscribe links hit `routes/unsubscribe.ts`. Outbound email builds SES headers in `utils/email.ts` (`X-SES-CONFIGURATION-SET`, default config set `erxes`) and sends through the SES transporter in [utils/transporter.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/broadcast/utils/transporter.ts) (`AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`). > [!WARNING] > **Stuck or suppressed sends** > > A broadcast stuck at `sending` means a batch worker is down or failing: compare `processedBatches` to `totalBatches` and check the worker logs; failed batches show in `failureCount` and `Logs`. If email never leaves, check `emailAddresses` for suppression lanes and `emailRampStatus` for throttling, then release with `emailAddressRelease`/`emailRampRelease`. ## Notifications `backend/core-api/src/modules/notifications/`: ```text graphql/schema/{index,subscription}.ts graphql/resolver/{queries,mutations,utils}.ts graphql/customResolvers/{notification,emailAddress}.ts db/models/{EmailSenders,EmailAddresses,EmailDeliveries,EmailRamp}.ts trpc/{email,index}.ts routes/{index,senderConfirm}.ts ``` Notification records, settings, and the outbound-email pipeline (senders, addresses, deliveries, ramp) share one module. In-app delivery uses the `notificationInserted` GraphQL subscription: ```graphql subscription OnNotification($id: String!) { notificationInserted(_id: $id) { _id } } ``` | Operation | Key arguments | | --------- | ------------- | | `notifications` | `ids`, `status` (`READ`/`UNREAD`/`ALL`), `priority` (`LOW`/`MEDIUM`/`HIGH`/`URGENT`), `type` (`INFO`/`SUCCESS`/`WARNING`/`ERROR`), `fromDate`, `endDate`, `fromUserId`, `module`, cursor params | | `notificationDetail` / `unreadNotificationsCount` | `_id!` / none | | `pluginsNotifications` | none; returns every plugin's declared modules/events | | `notificationSettings` | none; returns the per-user event/channel matrix | | `emailDeliveries` / `emailDeliveryDetail` | `status`, `source`, `provider`, `searchValue`, date range, cursor params / `_id!` | | `emailAddresses` | `lane`, `suppressionReason`, `searchValue`, `emails`, cursor params | | `emailRampStatus` | none | | `markNotificationAsRead` / `markAsReadNotifications` | `_id!` / notification filters | | `archiveNotification` / `archiveNotifications` | `_id!` / `ids`, `archiveAll`, `filters` | | `updateNotificationSettingsEvent` / `Channel` | `input: { event, enabled, channels }` / `input: { channel, enabled, metadata }` | | `emailAddressRelease` / `emailRampRelease` | `email!`, `note!` / `note!` | ### How plugins register notifications A plugin declares its notification modules and events in `src/meta/notifications.ts`: ```ts // backend/plugins/sales_api/src/meta/notifications.ts (trimmed) export const notifications = { plugin: 'sales', modules: [ { name: 'deals', description: 'Deals', icon: 'IconChecklist', events: [ { name: 'dealAssignee', title: 'Deal assignee', description: 'Triggered when a user is assigned to a deal' }, ], }, ], }; ``` `pluginsNotifications` merges these with `CORE_NOTIFICATION_MODULES` (`modules/notifications/constants.ts`) for the settings UI, where users toggle events and channels via `updateNotificationSettingsEvent`. To emit, a plugin calls `sendNotification(subdomain, { userIds, kind, notificationType, ... })` from [erxes-api-shared/core-modules](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/notifications/utils.ts); it validates the payload, creates the in-app record over tRPC (`module: 'notifications'`, `action: 'create'`), then `sendNotificationChannels` fans out to email (`sendNotificationEmail`) when the user has that channel enabled. Frontend plugins render entries in their own notification widgets under `frontend/libs/ui-modules` / plugin `NotificationsWidgets`. The email path is detailed in [Email Delivery](https://erxes.io/docs/api-email): `EmailSenders` (verified from-addresses, `senderConfirm` route), `EmailAddresses` (suppression lanes; release with `emailAddressRelease`), `EmailDeliveries` (per-message state), `EmailRamp` (sending warmup; release with `emailRampRelease`). ## Permissions `broadcastRead` (`always`), `broadcastCreate`, `broadcastUpdate`, `broadcastDelete`, `broadcastConfigsManage`; scope `own` (records in `createdBy`) or `all`. Notifications have no admin actions: users manage only their own feed and settings. ## Example ```graphql mutation SendTest { engageMessageSendTestEmail( from: "news@example.com" to: ["qa@example.com"] title: "Test" content: "

Hello

" ) } query MyNotifications { notifications(status: UNREAD, limit: 20) { list { _id } totalCount } } ``` --- # Email Delivery Transactional email transports, templates, and the Cloudflare mail worker How erxes delivers outbound mail and turns inbound mail into inbox conversations. There is no standalone Node "mail worker" service: inbound mail is handled by a **Cloudflare Email Worker** (`cloudflare/mail-worker`) plus the Frontline mail-integration module (see [Channel Integrations](https://erxes.io/docs/frontline-integrations)), and outbound mail goes through the shared `erxes-api-shared` email pipeline called by Core API, Broadcast, and the automations service. ## Outbound path `deliverEmail` in [erxes-api-shared/src/utils/email/service.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/email/service.ts) is the single send path. `resolveProvider` picks a transport from organization config (read via `loadEmailProviderConfig`, cached by config fingerprint): | `DEFAULT_EMAIL_SERVICE` | Provider | Config codes | | --- | --- | --- | | `SES` (default) | `SesEmailProvider` (AWS SES) | `AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_SES_CONFIG_SET` | | `sendgrid` | `SendgridEmailProvider` | `SENDGRID_API_KEY`, `SENDGRID_SUBUSER` | | `custom` | `SmtpEmailProvider` | `MAIL_SERVICE`, `MAIL_HOST`, `MAIL_PORT`, `MAIL_USER`, `MAIL_PASS` | Every send optionally flows through two ports defined in [core-api/src/utils/email/ports.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/utils/email/ports.ts): - **Delivery log**: `EmailDeliveries` records move `queued` → `sent`/`failed` via the `emailDeliveries.create`/`recordHandoff` tRPC procedures, capturing message id, provider response, and rejected recipients. - **Suppression**: `emailSuppression.blocked` filters suppressed recipients before the provider call. Callers: - `sendEmail` (`core-api/src/utils/email/index.ts`): transactional mail; applies `COMPANY_EMAIL_TEMPLATE`/`TYPE` Handlebars templates, `COMPANY_EMAIL_FROM`, sender alignment (`resolveAlignedFrom`/`alignSender`), and attachments. - Broadcast worker (`modules/broadcast/worker/email.ts`): bulk sends in chunks of 50 with a 2-second gap, unsubscribed-customer filtering, postal-address and unsubscribe links (`utils/email/links.ts`), and an 80%-failure abort. See [Broadcasts & Notifications](https://erxes.io/docs/core-broadcast-notifications). - Automations (`services/automations/.../emailAction/sendEmails.ts`): the send-email workflow action uses the same `deliverEmail`. - Auth (`modules/auth/utils.ts`): SaaS magic links go out through SendGrid directly. Sender addresses are picked per tenant: SaaS uses the organization fallback email; OS uses `COMPANY_EMAIL_FROM` when set (`resolveDefaultSenderEmail`). ### Templates and recipient quality - `sendEmail` renders `customHtml` through Handlebars with `customHtmlData`, falling back to the `COMPANY_EMAIL_TEMPLATE` org config, and post-processes the result through `modifier` hooks. - Broadcast builds its body from the documents module's block content (`blocksToHtml`/`replaceContent`), appends the configured postal address and an unsubscribe link, and screens new addresses through the proven-address list before sending (`claim`/`listProven` in `utils/email/ramp.ts` and `screenNewAddresses.ts`); an 80%-failure threshold aborts the batch rather than burning sender reputation. > [!WARNING] > **Outbound mail silently skipped** > > `NODE_ENV=test` short-circuits `sendEmail`, and on SaaS a missing fallback sender also drops the message. Check `EmailDeliveries` for a `failed` row with the provider error. ## Inbound path ### 1. Cloudflare Email Worker receives the message [cloudflare/mail-worker/src/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/cloudflare/mail-worker/src/index.ts) accepts messages up to 25 MiB, derives the tenant from the recipient address (`tenantFromAddress`), stores the raw payload and attachments, and enqueues `MAIL_QUEUE`. A queue consumer then POSTs the payload to the deployment's erxes endpoint with `x-erxes-timestamp` and an `x-erxes-signature` HMAC derived per tenant. Transient statuses (401/403/408/425/429) retry; other failures dead-letter to `erxes-mail-dlq`. A `/verify` route completes inbound-address verification. ### 2. Frontline receives and threads it `frontline_api` mounts `POST /mail/receive` (`src/modules/integrations/mail/routes.ts`) → `controller/receiveMessage.ts` verifies the signature, parses the tagged address (`parseTaggedAddress` → `replyTag`), and threads: `MailMessages.findByReplyTag` reopens the existing conversation when the tag matches, otherwise a new conversation starts. The message persists via `receiveInboxMessage`, then `pConversationClientMessageInserted` publishes the real-time event to widgets and inbox subscribers. Deliveries are keyed by the queued payload, and a `keepStored` response tells the worker to retain the stored blob, so replayed deliveries thread to the same conversation instead of duplicating it. ### 3. Integration lifecycle `messageBroker.ts` in the mail module handles address provisioning, integration create/update (`mailCreateIntegration`, `mailUpdateIntegration`), indexes, and sendability checks. Mail integrations appear in the Frontline inbox like any other channel. Worker bindings (`wrangler.toml` / the `Env` interface in `src/types.ts`): | Binding | Purpose | | --- | --- | | `MAIL_QUEUE` / `MAIL_DLQ` | Queue for accepted mail; dead-letter queue for permanent failures | | `MAIL_STORE` | R2 bucket for the JSON payload (`/.json`) and attachments, later served through `/attachments/?token=` | | `ERXES_ENDPOINT` / `ERXES_ENDPOINT_TEMPLATE` | Fixed delivery URL, or a template containing a `{tenant}` placeholder | | `MAIL_ROUTES` | Optional KV per-tenant endpoint/secret overrides | | `WEBHOOK_SECRET`, `DEFAULT_TENANT`, `ATTACHMENT_BASE_URL` | Master HMAC secret, fallback tenant, public base for attachment URLs | `routeFor` looks up the endpoint per tenant (KV override → fixed endpoint → template); `masterFor` uses the route's own secret or falls back to `WEBHOOK_SECRET`, which is then tenant-derived before signing, so each tenant's inbound HMAC is unique even when they share the master secret. > [!WARNING] > **Inbound mail never arrives** > > The worker route has no `endpoint` configured for the tenant (`routeFor`), or the HMAC secret drifted. Check the worker logs for `erxes responded` errors and the `erxes-mail-dlq` dead-letter queue. ## Delivery observability Email deliveries and proven addresses are first-class records: `emailDeliveries` and `emailAddresses` are cursor-paginated GraphQL queries on Core API. The team-member record table also has an email activity log (the `EmailDeliveries` and `EmailAddresses` tables plus an activity-log sheet on each member), so you can audit what the platform sent on a member's behalf. --- # Import & Export Bulk-load and extract records Bulk-load CSV files into any registered entity type and extract records to downloadable files through per-plugin BullMQ workers. For field definitions referenced by column mapping, see [Properties, Tags & Segments](https://erxes.io/docs/core-properties-tags-segments). ## Module layout ```text backend/core-api/src/modules/import-export/ graphql/schema/{import,export,common}.ts graphql/resolvers/{queries,mutations} db/definitions/{import,export}.ts, db/models/{Imports,Exports}.ts trpc/{import,export,templates,index}.ts utils/{getImportExportTypes,getRequiredPermissions,validateConfig}.ts workers/utils/errorFileHandler.ts routes.ts # GET /import-export/download-template backend/core-api/src/meta/import-export/ # startImportExportWorker setup backend/erxes-api-shared/src/core-modules/import-export/ worker.ts # BullMQ workers per plugin utils/{importBatchProcess,exportBatchProcess,importUtils, headerMatcher,tempWorkspace,...}.ts ``` Each entity type is identified as `:.` (for example `core:contacts.customers`, `core:contacts.leads`, `core:contacts.companies`, `core:product.product`, `core:organization.users`). ## How a plugin registers types Each service calls `startImportExportWorker` from its meta entry (core: [meta/import-export/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/meta/import-export/index.ts)): ```ts // backend/core-api/src/meta/import-export/index.ts export default async (app: Express) => startImportExportWorker({ pluginName: 'core', config: { import: importConfiguration, export: exportConfiguration }, app, }); ``` `startImportExportWorker` ([worker.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/import-export/worker.ts), idempotent per `pluginName`) starts two BullMQ workers on queues `-import-processor` and `-export-processor`, tunable with `IMPORT_EXPORT_IMPORT_CONCURRENCY`, `IMPORT_EXPORT_EXPORT_CONCURRENCY`, and `IMPORT_EXPORT_*_LIMITER_MAX`/`_DURATION_MS` env vars. The `import`/`export` configs map content types to module handlers; core dispatches to `contacts`, `product`, and `organization` handler sets (`createCoreModuleProducerHandler`): - Import: `importHandlers` + `process*Rows.ts` per entity (e.g. `contacts/meta/import-export/import/customers/processCustomerRows.ts`). - Export: `getExportHeaders`, `getExportData`, `build*ExportRow` per entity. Each registered type also lists its required permission actions (`customersImportManage`, `companiesImportManage`, `productsImportManage`, `teamMembersImportManage`, plus the `*ExportManage` twins), returned by `importExportTypes`. ## GraphQL API ### Imports | Operation | Key arguments | | --------- | ------------- | | `importColumnPreview` | `entityType!`, `fileKey!`, `fileName!`; returns detected columns (`header`, `key`, `confidence`, `status`, `sampleValues`) and target `fields` | | `importFields` | `entityType!`; returns available target fields with `required`, `type`, `options`, `example` | | `importStart` | `entityType!`, `fileKey!`, `fileName!`, `columnMapping: [{ index, header, key }]` | | `importProgress` | `importId!`; returns `status`, `progress`, `processedRows`/`totalRows`, `rowsPerSecond`, `estimatedSecondsRemaining` | | `activeImports` | `entityType` | | `importHistories` | `entityType`, `entityTypes`, `status`, `limit`, `cursor`, `direction`, `cursorMode` | | `importCancel` / `importRetry` / `importResume` | `importId!` | The `Import` record tracks `status`, `totalRows`, `processedRows`, `successRows`, `errorRows`, `importedIds`, `errorFileUrl`, `progress`, `elapsedSeconds`, `rowsPerSecond`, `estimatedSecondsRemaining`, `jobId`, `userId`, `subdomain`. ### Exports | Operation | Key arguments | | --------- | ------------- | | `exportHeaders` | `entityType!`, `filters`; returns `[{ label, key, isDefault, type }]` | | `exportStart` | `entityType!`, `filters`, `ids`, `selectedFields` | | `exportHistories` | `entityTypes`, `status`, `limit`, `cursor`, `direction`, `cursorMode` | | `exportCancel` / `exportRetry` | `exportId!` | The `Export` record tracks `status`, `totalRows`, `processedRows`, `fileKey`, `filters`, `ids`, `selectedFields`, `progress`, `lastCursor` (resume point), `errorMessage`, `jobId`. ### Shared | Operation | Key arguments | | --------- | ------------- | | `importExportTypes` | `operation: IMPORT` or `EXPORT`; returns registered types with their required `permissions` | ## Running an import ### 1. Upload the file and get a fileKey Imports read `fileKey` from the platform file storage (the same store `/read-file?key=` serves). The UI uploads the CSV first; the worker then streams it. `processCSVStream` ([importUtils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/import-export/utils/importUtils.ts)) parses one row at a time through `csv-parse` (BOM-stripped, quotes relaxed), so memory stays O(1 row) even for large files. ### 2. Preview and map columns Call `importColumnPreview` with the `fileKey`. It returns detected columns with `confidence`/`status` and the target `fields` (`required`, `type`, `options`). Download the canonical header set anytime from `GET /import-export/download-template?entityType=core:contacts.customers` (templates live in `trpc/templates.ts`). ### 3. Start and watch Call `importStart` with `columnMapping` (`index` → target `key`). Poll `importProgress` for `status`, `progress`, `rowsPerSecond`, `estimatedSecondsRemaining`. Failures collect into `errorRows` and a downloadable `errorFileUrl` (`workers/utils/errorFileHandler.ts`). `processCSVStream` aborts the job on stream errors, so a partial count means the file was truncated or unreadable rather than silently skipped. ### 4. Export round-trip Call `exportHeaders` to pick `selectedFields`, then `exportStart` with `filters` or explicit `ids`. When `status` completes, download from the generated `fileKey`. Exports resume from `lastCursor` after `exportRetry`. > [!WARNING] > **Job stays queued or every row errors** > > A job that never leaves the queue means the plugin's API is not running: workers start inside each service, not a separate process. Look for `[ImportExport] Worker ready for queue -import-processor` in the service logs. If every row errors, the column `key`s do not match `importFields`; rerun `importColumnPreview` and fix `columnMapping`. ## Permissions `importsManage` and `exportsManage` (core module `importExport`) gate the job operations themselves; each entity type additionally requires its own action listed by `importExportTypes` (for example `customersImportManage` for `core:contacts.customers`). ## Example ```graphql mutation StartCustomerImport { importStart( entityType: "core:contacts.customers" fileKey: "imports/2025/customers.csv" fileName: "customers.csv" columnMapping: [ { index: 0, header: "First Name", key: "firstName" } { index: 1, header: "Email", key: "primaryEmail" } ] ) { _id status } } query Watch($importId: String!) { importProgress(importId: $importId) { status progress processedRows totalRows errorRows errorFileUrl } } ``` --- # Build Plugins Extend erxes with backend services and frontend remotes Extend erxes with your own backend services and frontend remotes, from `pnpm create-plugin` to a deployed, permission-aware feature. ## Plugin anatomy A complete plugin is a pair of Nx projects: ```text backend/plugins/_api Express service: subgraph /graphql, /trpc router, meta extension hooks, BullMQ workers, one port each frontend/plugins/_ui Module Federation remote: ./config plus module, settings, and widget exposes ``` The `_api` service calls `startPlugin({ name, port, ... })` from `erxes-api-shared` ([start-plugin.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/start-plugin.ts)) and registers with the gateway through Redis service discovery. The gateway composes its subgraph once the service answers `_service { sdl }`. The `_ui` remote exposes `./config`, an `IUIConfig` object the `core-ui` host loads to build navigation, routes, settings entries, and widgets. Enable both at once by adding the base name (`inventory`, not `inventory_api`) to `ENABLED_PLUGINS`. ## The scope boundary erxes enforces a hard plugin boundary: - A backend-only task may write only under `backend/plugins/_api/**`; a frontend-only task only under `frontend/plugins/_ui/**`; a full-stack plugin task only under those two matching directories. - Never edit `core-api`, `core-ui`, another plugin, a shared library, or root infrastructure to make a plugin feature work, and never import another plugin's source, models, state, or components. - Use shared code only through the public APIs of `erxes-api-shared`, `erxes-ui`, and `ui-modules`. If the behavior cannot be implemented inside the boundary, stop and report the missing platform capability; a core or shared change is a separate task with explicit scope. ## Guides - [Create a Plugin](https://erxes.io/docs/create-plugin): Learn how to create an erxes plugin from scratch - [Standalone Plugins](https://erxes.io/docs/create-plugin-standalone): Develop a plugin in its own repository with create-erxes-plugin - [Backend Plugins](https://erxes.io/docs/plugin-backend): Tenant-aware API services with gateway registration - [Frontend Plugins](https://erxes.io/docs/plugin-frontend): Remotes loaded by Core UI - [GraphQL & tRPC](https://erxes.io/docs/plugin-graphql-trpc): Federated operations and service procedures - [Shared UI Components](https://erxes.io/docs/plugin-ui-components): Reuse erxes-ui and ui-modules - [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions): Permissions and cross-module hooks - [Workers & Background Jobs](https://erxes.io/docs/plugin-workers): BullMQ queues and delivery state Start with [Create a Plugin](https://erxes.io/docs/create-plugin) (or [Standalone Plugins](https://erxes.io/docs/create-plugin-standalone) for a separate repository), then read the generated code alongside [Backend Plugins](https://erxes.io/docs/plugin-backend) and [Frontend Plugins](https://erxes.io/docs/plugin-frontend). For the core modules your `meta` hooks and tRPC calls target, see [Core Platform](https://erxes.io/docs/core-overview); for how plugins register and load, see [Architecture](https://erxes.io/docs/architecture-overview). --- # Create a Plugin Learn how to create an erxes plugin from scratch 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](https://erxes.io/docs/create-plugin-standalone). ## Prerequisites - Complete [Local Setup](https://erxes.io/docs/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: ```bash pnpm create-plugin ``` Or pass both names for a repeatable run: ```bash 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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/create-plugin.js), [create-backend-plugin.js](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/scripts/create-backend-plugin.js)) writes these files: ```text 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(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`: ```dotenv 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 ```bash # 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](https://erxes.io/docs/plugin-backend) and [Frontend Plugins](https://erxes.io/docs/plugin-frontend) for the service and host APIs, and [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions) for permissions and hooks. ### 6. Check it works ```bash 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 `./` and `./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-` 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 - [Backend Plugins](https://erxes.io/docs/plugin-backend): startPlugin contract, tenant models, meta dispatch, endpoints - [Frontend Plugins](https://erxes.io/docs/plugin-frontend): IUIConfig, federation exposes, host loading - [GraphQL & tRPC](https://erxes.io/docs/plugin-graphql-trpc): Subgraph conventions, pagination, service calls - [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions): Permissions, automations, agent tools --- # Standalone Plugins Develop a plugin in its own repository with create-erxes-plugin 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](https://erxes.io/docs/create-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 name 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](https://erxes.io/docs/local-setup). Loading remotes from outside the monorepo in development is on `main` and part of the next release. - Node.js 22, or Bun for the Bun-based stacks. Any of npm, pnpm, yarn, or bun works as the plugin's package manager. - MongoDB and Redis reachable from the plugin, using the same instances as your erxes checkout. ## The shared libraries The erxes shared libraries are published on npm and developed in their own repositories: | Import name | npm package | Repository | Used by | | --- | --- | --- | --- | | `erxes-ui` | `@erxes/ui` | [erxes/ui](https://github.com/erxes/ui) | the UI: design-system primitives, hooks, state, `IUIConfig` | | `ui-modules` | `@erxes/ui-modules` | [erxes/ui-modules](https://github.com/erxes/ui-modules) | the UI: business modules such as contacts, products, team members | | `erxes-api-shared` | `@erxes/api-shared` | [erxes/api-shared](https://github.com/erxes/api-shared) | the API on the `platform` stack: `startPlugin`, tenant models, core modules | The generated manifests install them under the names erxes code imports, using npm aliases, so a plugin reads exactly like a monorepo plugin: ```jsonc // ui/package.json "erxes-ui": "npm:@erxes/ui@^1.0.0", "ui-modules": "npm:@erxes/ui-modules@^1.0.0" // api/package.json (platform stack) "erxes-api-shared": "npm:@erxes/api-shared@^1.0.0" ``` Each package has its own semver version. Keep the plugin on the versions your erxes deployment is built with: at runtime Core UI provides `erxes-ui` and `ui-modules` as Module Federation singletons, so the installed copies only decide what TypeScript and Rspack resolve against, and a mismatch shows up as broken imports rather than a helpful error. See [Shared Libraries](https://erxes.io/docs/architecture-shared-libraries) for what the packages provide. ## Install the CLI The CLI is published as `@erxes/create-plugin`. Every package manager's `create` shorthand reaches it: ```sh npm create @erxes/plugin@latest inventory pnpm create @erxes/plugin inventory yarn create @erxes/plugin inventory bun create @erxes/plugin inventory npx @erxes/create-plugin inventory ``` There is also a standalone binary that does not need Node.js: ```sh # macOS / Linux curl -fsSL https://raw.githubusercontent.com/erxes/create-erxes-plugin/main/install.sh | sh ``` ```powershell # 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 ` and `--dir ` after `sh -s --` to pin a release or choose the directory. The source is at [erxes/create-erxes-plugin](https://github.com/erxes/create-erxes-plugin). ## Generate and connect a plugin ### 1. Generate the repository ```bash npm create @erxes/plugin@latest inventory -- --backend platform --pm pnpm ``` The CLI asks for anything not passed as a flag: | Flag | Default | | --- | --- | | `[directory]` | the plugin name | | `-n, --name ` | the directory name | | `-t, --title ` | the name in Title Case, shown in erxes navigation | | `-d, --description <text>` | `<title> plugin for erxes` | | `-b, --backend <stack>` | asked interactively: `platform`, or a standalone framework (`express`, `fastify`, `hono`, `elysia`, `nestjs`) | | `--pm <npm\|pnpm\|yarn\|bun>` | the package manager running the CLI; `bun` runs the API on Bun | | `--api-port <port>` | `3399` | | `--ui-port <port>` | `3099` | | `--no-install`, `--no-git` | dependencies are installed and `git init` runs | | `-y, --yes` | accept defaults for every unanswered question | With `bun create`, pass `--yes` rather than `-y`, which Bun consumes itself. Plugin names are lowercase words separated by single dashes (`inventory`, `erxes-agent-v2`). The generated repository: ```text erxes.json plugin identity and ports, read by api/ and ui/ .env.example one .env at the root, read by both parts api/ the plugin API (see the next step) src/print-schema.ts prints the API's GraphQL schema for codegen ui/ React Module Federation remote with prefixed Tailwind CSS codegen.ts generates typed GraphQL documents from the printed schema Dockerfile API-only image for the chosen package manager .oxlintrc.json lint rules, including the codegen import guards README.md commands and the rules that keep the plugin working docs/erxes-integration.md how this plugin plugs into erxes, with its names filled in ``` ### 2. Pick the backend stack | Stack | What you get | Runtime | | --- | --- | --- | | `platform` | `startPlugin` 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 | | `express` | Express 5 with an Apollo Server federated subgraph | Node.js or Bun | | `fastify` | Fastify with an Apollo Server federated subgraph | Node.js | | `hono` | Hono with a GraphQL Yoga federated subgraph | Node.js or Bun | | `elysia` | Elysia with a GraphQL Yoga federated subgraph | Bun | | `nestjs` | NestJS with the `@nestjs/apollo` federation driver, code-first | Node.js | The standalone frameworks have 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. Use `platform` for plugins that store tenant data or call core modules; a standalone framework suits thin integrations that only need to appear in the supergraph, or teams that already run that framework. Either way, every GraphQL type and operation carries the plugin prefix (`Inventory`, `inventory`) because all plugins share one supergraph. See [Backend Plugins](https://erxes.io/docs/plugin-backend) for the `startPlugin` options and [GraphQL & tRPC](https://erxes.io/docs/plugin-graphql-trpc) for subgraph conventions. ### 3. Configure and run In the plugin repository, copy `.env.example` to `.env`. There is a single `.env` at the repository root; both `api/` and `ui/` read it. The generated defaults point at local Redis and announce the UI dev server: ```dotenv REDIS_HOST=localhost REDIS_PORT=6379 MONGO_URL=mongodb://localhost:27017/erxes # platform stack UI_ENTRY_URL=http://localhost:3099/remoteEntry.js ``` Start the plugin API first, then erxes with the plugin enabled, then the plugin UI: ```bash # 1. in the plugin repository pnpm run dev:api # registers erxes-service-inventory and erxesservice:config:inventory in Redis # 2. in the erxes checkout ENABLED_PLUGINS=sales,inventory pnpm dev:core-api # gateway + Core API ENABLED_PLUGINS=sales,inventory pnpm nx serve core-ui # 3. back in the plugin repository pnpm run dev:ui # runs codegen, then serves http://localhost:3099/remoteEntry.js ``` `ENABLED_PLUGINS` can also live in the erxes `.env`. Start the plugin API before the gateway: the gateway waits for every enabled plugin's address to appear in Redis before it composes the supergraph. Use `pnpm dev:core-api` and `pnpm nx serve core-ui` rather than `pnpm dev:apis` and `pnpm dev:uis`, which would also try to start a monorepo plugin of that name. Open `http://localhost:3001/inventory`. The remote's own port is never the app entry point. ### 4. Understand how it connects `ENABLED_PLUGINS` is a list of names, not paths. Every consumer turns a name into a Redis key and reads what the plugin wrote there: | Who | What it does with `inventory` | | --- | --- | | plugin API | on start, writes `erxes-service-inventory` (its address) and `erxesservice:config:inventory` (including `uiEntry` from `UI_ENTRY_URL`) to Redis | | gateway | for each name in `ENABLED_PLUGINS`, reads the address from Redis, fetches that subgraph's schema, and composes the supergraph | | Core API | reads `uiEntry` from Redis and returns `{ name: "inventory_ui", entry }` from `GET /get-frontend-plugins` | | Core UI | loads `remoteEntry.js` from that URL, reads `inventory_ui/config`, adds the navigation entry, and routes `/inventory/*` to the remote | Requests from the page reach the plugin through the gateway at `/graphql`, which forwards the signed-in user and tenant. The generated status query on the overview page requires a signed-in user, so it answers `Login required` to an anonymous request. ### 5. Work with GraphQL types The generated repository follows the same schema-driven codegen as the erxes `operation` plugin. `pnpm run codegen` prints the API's subgraph SDL to `api/generated/schema.graphql`, then `ui/codegen.ts` (graphql-codegen client preset) validates every document under `ui/src` against it and generates typed documents into `ui/src/gql/`. Both folders are gitignored; `check`, `build`, and `dev:ui` run `codegen` first. ```ts import { gql } from "~/gql"; export const STATUS_QUERY = gql(` query inventoryStatus { inventoryStatus { plugin version subdomain userEmail } } `); const { data } = useQuery(STATUS_QUERY); // data.inventoryStatus is typed from the SDL ``` Write documents as `gql()` calls on static strings with no `${}` interpolation, and let Apollo hooks infer their types: no `useQuery<T>` generics and no handwritten response interfaces. A field the SDL does not have, a wrong argument, or a nullable field treated as required fails `check` instead of surfacing as a runtime Apollo error. `.oxlintrc.json` rejects `graphql-tag` and `gql` from `@apollo/client` under `ui/` so every document is one codegen sees. On the `nestjs` stack, register every resolver class in `api/src/resolvers.ts`; `AppModule` and `print-schema.ts` read the same list. ### 6. Check it works ```bash pnpm run check && pnpm run lint && pnpm run build ``` `check` runs codegen and type-checks both parts; `build` emits `api/dist` and `ui/dist/remoteEntry.js`. Then check end to end: - Redis has `erxes-service-inventory` and `erxesservice:config:inventory` with `uiEntry`, and the gateway log shows the plugin joined service discovery. - `GET http://localhost:3300/get-frontend-plugins` lists `inventory_ui` with your `UI_ENTRY_URL`. - 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. ### 7. 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 works against a deployed Core UI once its API and remote are reachable. See [Deployment](https://erxes.io/docs/self-hosting) for the erxes side. ## Names that must stay consistent All of these come from `erxes.json`; change them only there, and only together. | Name | Example | Used by | | --- | --- | --- | | `name` | `inventory` | Redis keys, `ENABLED_PLUGINS`, the docker host `plugin-inventory-api`, the CDN folder `inventory_ui` | | `ui.name` | `inventory` | `CONFIG.name`, the page expose `./inventory`, permission checks | | `ui.remote` | `inventory_ui` | the Module Federation container; Core API derives it from `name` | | `ui.path` | `inventory` | the route `/inventory/*` in Core UI | | GraphQL prefix | `inventory` / `Inventory` | operation, field, and type names, unique across the supergraph | Core UI expects the remote to expose `./config` (the `IUIConfig` export named `CONFIG`), `./<ui.name>`, `./<ui.name>Settings`, and `./notificationWidget`; the generated `ui/rspack.config.ts` reads the names from `erxes.json`. The remote's `hostShared` list 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. Import from the package root (`import { Button } from "erxes-ui"`); a deep import such as `erxes-ui/components/button` would bypass the singleton and bundle a second copy. 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` warns that `ui-modules` has an unmet `erxes-ui` peer: pnpm cannot compare an aliased peer range. The peer is still linked to the `erxes-ui` the repository installs, so the warning is harmless. - `check` fails inside codegen: a document names a field or argument the SDL does not have, or two operations share a name. Fix the document, or the schema and its resolver together; the error lists the file and the offending selection. - 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 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 gateway starts but never finishes composing: it is waiting for an enabled plugin whose `erxes-service-<name>` key is missing. Start the plugin API, or remove the name from `ENABLED_PLUGINS`. - 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 rejects the subgraph: check that the plugin's GraphQL types carry the prefix. A bare `Query` field name used by another plugin fails composition. - `bun` with `platform`, `fastify`, or `nestjs` stops with an error: those stacks run on Node.js only. Use `express`, `hono`, or `elysia` with Bun, or switch to Node.js. ## Next steps - [Backend Plugins](https://erxes.io/docs/plugin-backend): startPlugin options, tenant models, meta dispatch - [Frontend Plugins](https://erxes.io/docs/plugin-frontend): IUIConfig, federation exposes, host loading - [Shared UI Components](https://erxes.io/docs/plugin-ui-components): Reuse erxes-ui and ui-modules - [Shared Libraries](https://erxes.io/docs/architecture-shared-libraries): What erxes-api-shared, erxes-ui and ui-modules provide --- # Backend Plugins Tenant-aware API services with gateway registration Build a tenant-aware API service that registers with the gateway through Redis service discovery. Start with [Create a Plugin](https://erxes.io/docs/create-plugin). For schema and service-call conventions see [GraphQL & tRPC](https://erxes.io/docs/plugin-graphql-trpc); for the `meta` extension points see [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions). ## `startPlugin` options Every backend plugin's `src/main.ts` calls [`startPlugin(configs)`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/start-plugin.ts) from `erxes-api-shared/utils`. The options type (`ConfigTypes`) accepts: | Option | Type | Purpose | | --- | --- | --- | | `name` | `string` | Plugin name used for service discovery, Sentry tags, and queue prefixes. Must match the `ENABLED_PLUGINS` entry. | | `port` | `number` | Listen port; `process.env.PORT` overrides it at runtime. | | `graphql` | `() => Promise<{ typeDefs, resolvers }>` | Builds the federated subgraph served at `/graphql`. | | `apolloServerContext` | `(subdomain, context, req, res) => Promise<IMainContext>` | Extends the per-request context; attach `models` here. | | `trpcAppRouter` | `{ router, createContext }` | Mounts `/trpc` for service-to-service calls. | | `expressRouter` | `express.Router` | Extra routes mounted before middlewares (webhooks, file routes). | | `middlewares` | `any[]` | Additional Express middleware. | | `apiHandlers` | `{ method, path, resolver }[]` | Simple REST handlers wrapped in the shared `logHandler`. | | `corsOptions` | `cors` options | Passed to `cors()`. | | `hasSubscriptions` | `boolean` | Requires `subscriptionPluginPath`; serves the subscription bundle. | | `subscriptionPluginPath` | `string` | File served at `GET /subscriptionPlugin.js` (rate-limited). | | `onServerInit` | `(app) => Promise<void>` | Runs after registration; plugins start MQ workers here. | | `meta` | `IMeta` | Extension-point config; see below. | A minimal `main.ts` ([sales](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/main.ts) uses every option): ```ts startPlugin({ name: 'inventory', port: 3314, graphql: async () => ({ typeDefs: await typeDefs(), resolvers }), apolloServerContext: async (subdomain, context) => { context.models = await generateModels(subdomain); return context; }, trpcAppRouter: { router: appRouter, createContext: async (subdomain, context) => { context.models = await generateModels(subdomain); return context; }, }, }); ``` ## Endpoints mounted for you | Endpoint | Condition | Notes | | --- | --- | --- | | `GET /health` | always | Returns `ok`; used for health checks. | | `GET /debug-sentry` | always | Throws a test error for Sentry. | | `POST /graphql` | always | Apollo Server v4 subgraph (`buildSubgraphSchema`). | | `* /trpc` | `trpcAppRouter` | tRPC express adapter with the shared context factory. | | `GET /subscriptionPlugin.js` | `hasSubscriptions` | Serves the file at `subscriptionPluginPath`, rate-limited to 1000 req/15 min/IP. | The server sets `keepAliveTimeout` to 120 s and `headersTimeout` to 121 s, parses JSON bodies up to 15 MB, and registers a SIGINT/SIGTERM handler that closes HTTP, closes Mongoose, and leaves service discovery. ## Tenant isolation with `generateModels` Every request carries a `subdomain`, the tenant the request belongs to, derived from the request host by `getSubdomain`. `connectionResolvers.ts` defines `loadClasses(db)`, which registers your Mongoose models on a connection, and exports `generateModels = createGenerateModels<IModels>(loadClasses)` ([generate-models.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mongo/generate-models.ts)). - Self-hosted (`VERSION !== 'saas'`): models bind to the single shared `mongoose.connection`. - SaaS: the subdomain maps to an organization and models bind to `useDb('erxes_<organizationId>', { useCache: true })`. Because `apolloServerContext` and `trpcAppRouter.createContext` run per request, models are always scoped to the requesting tenant. Never cache models across requests and never query another plugin's collections; use [tRPC calls](https://erxes.io/docs/plugin-graphql-trpc) for cross-service reads. ## GraphQL composition and the resolver pipeline The generated plugin composes `typeDefs` from `apolloCommonTypes` plus per-module `types`/`queries`/`mutations` strings under `extend type Query` / `extend type Mutation`. Resolvers merge per-module query/mutation objects, `apolloCustomScalars`, and custom type resolvers. `startPlugin` wraps every `Query` and `Mutation` resolver with `wrapApolloResolvers`, which applies, in order: 1. `withBeforeResolvers`: runs registered `meta.beforeResolvers` handlers, which can rewrite args or resolve the call early. 2. `wrapPermission`: rejects unauthenticated calls (`checkLogin`), unless the resolver carries `wrapperConfig` (`skipPermission`, `forClientPortal`, `cpUserRequired`). 3. `withLogging`: mutations only; sends the call to the logs pipeline. 4. `withSentryCapture`: reports non-`EXPECTED` errors to Sentry with operation tags. The context your resolvers receive (`IMainContext` plus whatever `apolloServerContext` adds) includes `user`, `cpUser`, `clientPortal`, `subdomain`, `processId`, `eventHandlers`, `__(doc)` (stamps the process id), and `checkPermission(action)` for per-action permission checks. ## The `meta` object `meta` does two things: `joinErxesGateway` serializes it into the Redis key `erxesservice:config:<name>` for core and the gateway to read, and `startPlugin` dispatches selected keys to local subsystems: | `meta` key | Startup dispatch | | --- | --- | | `automations` | `startAutomations`: tRPC producers on the app | | `segments` | `initSegmentProducers` | | `beforeResolvers` | `startBeforeResolvers` | | `afterProcess` | `startAfterProcess` | | `references` | `initRecordReferences` | | `notifications` | `initializePluginConfig(name, 'notifications', …)` | | `importExport` | `startImportExportWorker`: BullMQ workers | | `payments` | `startPayments`: BullMQ worker | `permissions`, `tags`, `documents`, `properties`, `relations`, and `logs` are serialized only; core-api reads them from the service-discovery config. The full `IMeta` type and every key are in [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions). ## Service discovery and ports On startup `joinErxesGateway` ([service-discovery.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/service-discovery.ts)) writes two Redis keys. `erxes-service-<name>` holds the address core and the gateway call; `erxesservice:config:<name>` holds JSON config (`dbConnectionString`, `hasSubscriptions`, `meta`, `releaseVersion`). The address is `LOAD_BALANCER_ADDRESS` if set, else `http://localhost:<port>` in development and `http://plugin-<name>-api:<port>` in production. In production the plugin also enqueues a delayed `gateway/update-apollo-router` BullMQ job so the gateway recomposes the supergraph. The gateway reads `ENABLED_PLUGINS` (plus `ENABLED_PLUGINS_ONLY_API` for API-only plugins) to build the active plugin list, always `core` first. Choose an unused port: `3303`–`3305`, `3307`–`3313`, and `33010` are taken (core `3300`, gateway `4000`). --- # Frontend Plugins Remotes loaded by Core UI Build a Module Federation remote that the core-ui host loads at runtime. For the generator and first run see [Create a Plugin](https://erxes.io/docs/create-plugin); for component reuse see [Shared UI Components](https://erxes.io/docs/plugin-ui-components); for host internals see [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend). ## `src/config.tsx`: the `IUIConfig` object Every remote exposes `./config`, which must export `CONFIG` typed as [`IUIConfig`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/libs/erxes-ui/src/types/UIConfig.ts) (from `erxes-ui`). The host loads it once at startup and builds navigation, routes, settings, and widgets from it: | Field | Type | Purpose | | --- | --- | --- | | `name` | `string` | Remote name without `_ui`; the host loads exposes `./<name>` and `./<name>Settings`. | | `path` | `string` | URL prefix; the host mounts `/<path>/*`. | | `icon` | `React.ElementType` | Optional plugin icon (use `@tabler/icons-react`). | | `navigationGroup` | `{ name, defaultPath?, icon, content(), subGroup?() }` | Sidebar group; `content`/`subGroup` render lazy nav components. | | `settingsNavigation` | `() => ReactNode` | Entries under Settings. | | `modules` | `{ name, icon?, path, hasAutomation?, hasRelationWidget?, hasFloatingWidget?, hasSegmentConfigWidget? }[]` | Feature modules; `path` is relative to `path`. | | `widgets` | `{ relationWidgets?, customerDetailWidgets?, formWidgets?, propertyInputs?, activityRows? }` | Named widgets other modules render. | | `searchProviders` | `ISearchProvider[]` | Global-search providers. | | `i18n`, `i18nNamespace` | `boolean`, `string` | Translation namespace the host loads. | | `hasFloatingWidget`, `settingsOnly` | `boolean` | Behavior flags. | [sales_ui's config](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/sales_ui/src/config.tsx) is the worked example: `navigationGroup` with `defaultPath: 'sales/deals'` and a `subGroup`, four `modules`, `relationWidgets`, a `propertyInputs` map, and `searchProviders`. ## `module-federation.config.ts` The generated config declares `name: '<name>_ui'` and four exposes: ```ts exposes: { './config': './src/config.tsx', './inventory': './src/modules/InventoryMain.tsx', './inventorySettings': './src/modules/InventorySettings.tsx', './widgets': './src/widgets/Widgets.tsx', }, ``` The expose names `./<name>` and `./<name>Settings` are fixed, not a convention: the host's [`usePluginsRouter`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/modules/app/hooks/usePluginsRouter.tsx) mounts `/<path>/*` to `loadRemote('<name>_ui/<name>')` and settings routes to `loadRemote('<name>_ui/<name>Settings')`. Rename or move an expose and the host 404s. Sales adds more exposes (`./relationWidget`, `./notificationWidget`, `./automationsWidget`, …) that other modules request by name. The `shared` function returns the default config for the `coreLibraries` set (`react`, `react-dom`, `react-router`, `react-router-dom`, `erxes-ui`, `@apollo/client`, `jotai`, `ui-modules`, `react-i18next`) and `false` for everything else. These become singletons shared with the host; anything outside the set is bundled per-plugin. `rspack.config.ts` composes `withNx()`, `withReact()`, and `withModuleFederation(config, { dts: false })`; `rspack.config.prod.ts` re-exports it. Both need their default exports; the Nx/Rspack tooling is the one place a default export is allowed. ## How the host loads remotes - **Development**: `pnpm dev:uis` runs `nx serve core-ui --devRemotes="<name>_ui …"`. Each plugin's `serve` target (`@nx/rspack:module-federation-dev-server`) serves `remoteEntry.js` on its `project.json` port; core-ui's `remotes` list is generated from `ENABLED_PLUGINS` as `<name>_ui` names. - **Production**: core-ui's `bootstrap.tsx` fetches `${REACT_APP_API_URL}/get-frontend-plugins`, which returns `{ name, entry }` objects pointing at `https://plugins.erxes.io/<releaseVersion>/<name>_ui/remoteEntry.js`, and calls `init({ remotes })` at runtime. The nginx config disables caching on `remoteEntry.js`. At runtime [`RenderPluginsComponent`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/plugins/components/RenderPluginsComponent.tsx) calls `loadRemote('<pluginName>/<remoteModuleName>')` and finds a component from the module using a candidate-key list (`default`, PascalCase name, `<Name>Component`, `<Name>RemoteEntry/RemoteEntries/Widget`, `AutomationRemoteEntries`). All exposed modules must still use named exports; never rely on `default` to satisfy the loader. ## Routing inside the remote The host mounts `/<path>/*`; your exposed main component serves everything below it with its own `<Routes>`: ```tsx // generated pattern — sales does the same with /deals and /pos export const InventoryMain = () => ( <Suspense fallback={<Spinner />}> <Routes> <Route index element={<Navigate to="items" replace />} /> <Route path="/items" element={<IndexPage />} /> </Routes> </Suspense> ); ``` Keep `config.tsx` `path`, `modules[].path`, `navigationGroup.defaultPath`, and these routes aligned; a path the config advertises but no route serves renders an empty remote. ## i18n Set `i18n: true` (namespace = `name`) or `i18nNamespace: '<ns>'` in `CONFIG`. The host calls `i18n.loadNamespaces(ns)`, which fetches `${REACT_APP_API_URL}/locales/<lng>/<ns>.json`. The gateway serves those from `backend/gateway/src/locales/<lng>/<file>` and falls back to asking each plugin API for `/locales/<lng>/<file>`, so a plugin can serve its own namespace by mounting a `/locales` route through `startPlugin`'s `expressRouter`. `react-i18next` is a shared singleton, so `useTranslation()` works directly in plugin code. ## Data, state, and the real-time rule - Apollo Client (shared singleton) for server state; keep GraphQL documents next to the feature and prefix operation names with the plugin or module. - Jotai for plugin-wide client state, React state for component-local state. - React Hook Form + Zod for forms; provide loading, empty, success, and error states. No button without a handler. - After any create/update/delete, update the Apollo cache, refetch the affected query, or `subscribeToMore`; the UI must never require a manual refresh. - Lazy-load exposed modules inside `Suspense`. ## Ports The generated `project.json` serve port is `3005`, which `sales_ui` already uses. Ports already in use: `3002`–`3011` (content `3003`, frontline `3004`, sales `3005`, operation `3006`, the rest held by Enterprise Edition remotes) plus host `3001`. Pick a free port. --- # GraphQL & tRPC Federated operations and service procedures Expose a federated GraphQL subgraph for UI reads and tRPC procedures for service-to-service calls. For auth headers see [API Authentication](https://erxes.io/docs/api-authentication); for how the gateway composes subgraphs see [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway). ## Federated subgraph conventions `startPlugin` builds your `typeDefs`/`resolvers` into a subgraph (the plugin's part of the composed schema) with `buildSubgraphSchema` and serves it at `/graphql`. The gateway polls each service's subgraph endpoint and composes the supergraph with `rover supergraph compose`. - Compose `typeDefs` from `apolloCommonTypes` (scalars `Date`/`JSON`, `CURSOR_DIRECTION`, `CURSOR_MODE`, `Attachment`, `Coordinate`, `PageInfo`) plus per-module schema strings under `extend type Query` / `extend type Mutation`. - Operation names are unique repo-wide and prefixed with the plugin or module: `salesPipelines`, `frontlineInbox`, `cmsPageList`. Client-facing client-portal operations use the `cp` prefix (`cpSalesPipelines`). - Keep schema and resolvers inside their module's `graphql/` directory. ## Pagination Two parameter styles exist and are not interchangeable: | Style | Schema fragment | Resolver helper | | --- | --- | --- | | Cursor | `GQL_CURSOR_PARAM_DEFS` (`limit`, `cursor`, `cursorMode`, `direction`, `orderBy`, `sortMode`, `aggregationPipeline`) | `cursorPaginate` / `cursorPaginateAggregation` | | Offset | `GQL_OFFSET_PARAM_DEFS` (`page`, `perPage`, `sortField`, `sortDirection`) | `defaultPaginate` | `cursorPaginate({ model, params, query, formatter })` ([mongoose-utils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mongo/mongoose-utils.ts)) enforces `limit` 1–100 (default 20), decodes the base64 cursor, applies `orderBy` with an `_id` tiebreaker, and returns `{ list, totalCount, pageInfo }`. Declare `formatter` for typed sort fields (`createdAt: 'date'`). A sales example: ```graphql # modules/sales/graphql/schemas/pipeline.ts salesPipelines( boardId: String, isAll: Boolean limit: Int cursor: String cursorMode: CURSOR_MODE direction: CURSOR_DIRECTION orderBy: JSON sortMode: String aggregationPipeline: [JSON] ): SalesPipelinesListResponse ``` ```ts const { list, totalCount, pageInfo } = await cursorPaginateAggregation<IPipelineDocument>({ model: models.Pipelines, pipeline: [{ $match: query }, PIPELINE_STATUS_RANK_STAGE], params: { ...params, orderBy: PIPELINES_ORDER_BY, limit: params.limit || 20 }, formatter: { createdAt: 'date' }, }); ``` ## The resolver pipeline and `wrapperConfig` `wrapApolloResolvers` wraps every Query/Mutation resolver: `beforeResolvers` hooks run first, then `wrapPermission` calls `checkLogin(user)`, mutations go through `withLogging`, and everything ends in `withSentryCapture`. Per-resolver overrides live on `resolver.wrapperConfig`: ```ts // opt out of the login check for client-portal traffic (sales wishlist) wishlistQueries.cpWishlist.wrapperConfig = { forClientPortal: true }; // or mark a whole resolver map (sales productReview mutations) markResolvers(productReviewMutations, { wrapperConfig: { skipPermission: true }, }); ``` - `forClientPortal` requires `context.clientPortal`; add `cpUserRequired: true` to also require `context.cpUser`. - `skipPermission` makes the resolver public; use it sparingly. - Inside resolvers, `context.checkPermission('<action>')` enforces a registered action and OAuth scope; `context` also carries `user`, `subdomain`, `models`, `eventHandlers`, `__(doc)`, and `processId`. ## tRPC routers The generated `src/trpc/init-trpc.ts` creates `t = initTRPC.context<ITRPCContext>().create()` and an `appRouter`. Plugins type the context with their models and merge module routers; sales: ```ts export type SalesTRPCContext = ITRPCContext<{ models: IModels }>; const t = initTRPC.context<SalesTRPCContext>().create(); export const appRouter = t.mergeRouters( dealTrpcRouter, posTrpcRouter, documentTrpcRouter, // … ); ``` Procedures are addressed as `<module>.<action>`. From sales' `dealTrpcRouter`: ```ts export const dealTrpcRouter = t.router({ deal: { findOne: t.procedure .input(z.any()) .query(async ({ ctx, input }) => { return await ctx.models.Deals.findOne(input).lean(); }), find: t.procedure .input(z.any()) .query(async ({ ctx, input }) => { const { query, skip, limit, sort = {} } = input; if (!query) return ctx.models.Deals.find(input).lean(); return ctx.models.Deals.find(query).skip(skip || 0).limit(limit || 0).sort(sort).lean(); }), }, }); ``` ## Calling other services Use `sendTRPCMessage` ([trpc/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/trpc/index.ts)): it looks up the target's address from Redis service discovery, skips disabled plugins, encodes the tenant and caller context into the `x-trpc-context` header, and returns `defaultValue` on failure unless `throwOnError` is set: ```ts const fields = await sendTRPCMessage({ subdomain, pluginName: 'core', method: 'query', module: 'fields', action: 'find', input: { query: { contentType: 'sales:deal' } }, defaultValue: [], }); ``` The callee sees `ctx.subdomain` plus optional `userId`/`processId` from the header; the `/trpc` mount rejects mutation calls that carry no context. For a raw client, the generated `src/trpc/trpcClients.ts` builds `createTRPCUntypedClient` with `httpBatchLink({ url: (await getPlugin('core')).address + '/trpc' })` after `isEnabled('core')`. On SaaS, `sendTRPCMessage` instead routes through `https://<subdomain>.next.erxes.io/gateway/pl:<plugin>/trpc`. ## Subscriptions over Redis Pub/sub runs on `graphqlPubsub`, a `RedisPubSub` instance from `erxes-api-shared/utils`. A plugin declares subscriptions in a dedicated file (`src/apollo/subscription.ts`) exporting `{ name, typeDefs, generateResolvers(graphqlPubsub) }`: ```ts export default { name: 'sales', typeDefs: `salesDealChanged(_id: String!): DealSubscription`, generateResolvers: (graphqlPubsub) => ({ salesDealChanged: { resolve: (payload) => payload.salesDealChanged, subscribe: (_, { _id }) => graphqlPubsub.asyncIterator(`salesDealChanged:${_id}`), }, }), }; ``` Set `hasSubscriptions: true` and `subscriptionPluginPath` in `startPlugin`. The file is served at `/subscriptionPlugin.js`; the gateway's subscription service downloads each plugin's bundle ([downloadPlugins.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/subscription/plugins/downloadPlugins.ts)) to build the composite subscription resolver. Publish with `graphqlPubsub.publish('<channel>', payload)` from resolvers or `afterProcess` handlers. --- # Shared UI Components Reuse erxes-ui and ui-modules Reuse the `erxes-ui` primitives and `ui-modules` business modules instead of building parallel components. Both libraries are Module Federation singletons; the host shares one copy with every remote. For how remotes load, see [Frontend Plugins](https://erxes.io/docs/plugin-frontend). ## What each library exports `erxes-ui` ([index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/libs/erxes-ui/src/index.ts)) barrels seven areas: | Barrel | Contents | | --- | --- | | `components` | ~60 primitives: `Button`, `Form`, `Sheet`, `Dialog`, `Select`, `Combobox`, `Table`, `Tabs`, `Sidebar`, `CommandBar`, `Command`, `Breadcrumb`, `Tooltip`, `Spinner`, `Skeleton`, `Empty`, `Toast`/`Toaster`, `Dropzone`, date/time pickers, and more | | `modules` | Composed feature blocks: `RecordTable`, `board`, `filter`, `record-field`, `inputs`, `select-tree`, `select-operation`, `navigation-menu`, `side-menu`, `focus-sheet`, `attachments`, `preview`, `report`, `display`, `layout`, `icons`, `blocks`, `hotkey` | | `hooks`, `state`, `states` | Shared hooks and Jotai atoms (e.g. `useConfirm`, toast state) | | `types` | `IUIConfig`, `TPropertyInputProps`, search-provider types | | `constants`, `lib`, `utils` | Env constants (`NODE_ENV`, `REACT_APP_API_URL`), helpers | `ui-modules` ([index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/libs/ui-modules/src/index.ts)) exports business-domain modules plus `hooks` and `states`: - `header`: `PageHeader` with `Start`/`End`/`FavoriteToggleButton` composition - `contacts`, `products`, `sales`, `segments`, `tags`, `properties`, `permissions`, `structure`, `team-members`, `brands` - `automations`, `approval`, `documents`, `import-export`, `internal-notes`, `activity-logs`, `notifications`, `payments`, `templates`, `widget`, `favorites`, `shared`, `tags-new` - Plugin-facing atoms such as `pluginsConfigState` (the loaded `IUIConfig` map) ```tsx import { Button, Form, RecordTable, Sheet } from 'erxes-ui'; import { PageHeader } from 'ui-modules'; ``` ## Composed components to reach for first Prefer composed components over hand-rolled layouts: - `RecordTable` ([record-table](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/libs/erxes-ui/src/modules/record-table/index.ts)): `Object.assign(RecordTableRoot, { Provider, Header, Head, InlineHead, RowList, Body, Cell, Row, RowSkeleton, MoreButton, checkboxColumn, Scroll, CursorProvider, CursorBackwardSkeleton, CursorForwardSkeleton, ColumnSelector })` plus `useRecordTable` and `useRecordTableCursor` for cursor-paginated lists. - `Form.Field`: React Hook Form field wrapper (with `Form.useField`) for label/control/message per field. - `Sheet.Content`: the side panel used for detail/edit flows. - `CommandBar`: the bulk-action bar for selected table rows (`Object.assign(Root, { ... })` composition). - `filter` module: query-state-driven filter bars and date filters. - `NavigationMenuLinkItem` / `SettingsNavigationMenuLinkItem`: the generated navigation uses these for plugin sidebar entries. - `PageHeader` (ui-modules): standard page chrome; the generated `IndexPage` already composes it with `Breadcrumb` and `Separator.Inline`. - `select-operation`, `select-tree`: member/company/segment-style selectors. Icons come from `@tabler/icons-react` (e.g. `IconSandbox`, `IconBriefcase`); `erxes-ui` also includes `IconsProvider`, `IconComponent`, and `IconPicker` for user-chosen icons. ## A full composition `sales_ui`'s `PipelineRecordTable` shows the intended stack in one file: table state from URL query state, confirmation on destructive actions, translations via the plugin namespace: ```tsx import { RecordTable, RecordTableTree } from 'erxes-ui'; <RecordTable.Provider columns={pipelinesColumns(t)} data={pipelines || []} stickyColumns={['more', 'checkbox', 'name']} > <PipelineCommandBar /> <RecordTableTree id="pipelines-list" ordered> <RecordTable.Scroll> <RecordTable> <RecordTable.Header /> <RecordTable.Body> <RecordTable.RowList Row={RecordTableTree.Row} /> {loading && <RecordTable.RowSkeleton rows={30} />} {!loading && pageInfo?.hasNextPage && ( <RecordTable.RowSkeleton rows={1} handleInView={handleFetchMore} /> )} </RecordTable.Body> </RecordTable> </RecordTable.Scroll> </RecordTableTree> </RecordTable.Provider> ``` Cursor-style infinite scroll is a skeleton row whose `handleInView` calls the page's fetch-more handler; the same file uses `useConfirm` for destructive actions and `useTranslation('sales')` for the plugin's i18n namespace. ## Find the pattern before you build - `rg "RecordTable.Provider" frontend/plugins --files-with-matches`: find every plugin already using a component, then read the closest one. - `frontend/libs/erxes-ui/src/components/__stories__` and `src/modules/__stories__`: per-component Storybook stories (`button.stories.tsx`, `commandbar.stories.tsx`, `form.stories.tsx`) show intended usage. - `rg "<ComponentName>" frontend/libs/ui-modules/src`: find the shared wrapper before composing your own (a `SelectMember`-style module may already exist under `contacts`, `team-members`, or `segments`). ## Rules - **No direct `@radix-ui/*` imports and no hand-rolled primitives**: `erxes-ui` components are already the Radix layer; the host style system assumes them. - **Search before building**: find a similar implementation before writing new code. Before a new list, form, or detail view, open a plugin that already has one (`sales_ui` deals, `operation_ui` tasks, `frontline_ui` inbox) and reuse its pattern without importing its source. - Import only through the package names (`erxes-ui`, `ui-modules`); never deep-import `frontend/libs/*/src/...` paths, and never import another plugin's files. - Keep plugin-specific components, hooks, translations, and utilities inside `frontend/plugins/<name>_ui/src/` (aliases `~/*` → `src`, `@/*` → `src/modules`). - Match nearby spacing, loading, empty, and error patterns; avoid inline styles unless the neighboring code uses them for the same pattern. --- # Plugin Metadata & Extensions Permissions and cross-module hooks Register permissions, automations, segments, and other cross-module hooks so core and the gateway can discover plugin behavior. The `meta` object passed to `startPlugin` does two things: `joinErxesGateway` serializes it into the Redis key `erxesservice:config:<name>` (read by core-api, the gateway, and other services through `getPlugin`), and `startPlugin` mounts or starts the subsystems that need live producers. See [Backend Plugins](https://erxes.io/docs/plugin-backend) for the `startPlugin` options. ## Extension-point keys The `IMeta` keys from [start-plugin.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/start-plugin.ts), with who consumes them: | `meta` key | Config shape | Consumer | | --- | --- | --- | | `permissions` | `IPermissionConfig` | core-api permission resolvers, group-action map, OAuth scopes (via Redis config) | | `automations` | `AutomationConfigs` | `startAutomations` mounts tRPC producers; `automations-service` calls them | | `segments` | `SegmentConfigs` | `initSegmentProducers`; core segments engine and logs service call them | | `documents` | `{ types: { label, contentType }[] }` | core documents (serialized) | | `notifications` | `{ plugin, modules[{ name, icon, events[] }] }` | `initializePluginConfig`; core notifications (serialized) | | `importExport` | `ImportExportConfigs` | `startImportExportWorker`: BullMQ workers on `<plugin>-import-processor` / `-export-processor` | | `payments` | `{ <jobName>: (ctx, data) => … }` | `startPayments`: BullMQ worker on `<plugin>-payments` | | `beforeResolvers` | `BeforeResolversConfig` | `startBeforeResolvers`; every GraphQL resolver runs through it | | `afterProcess` | `AfterProcessConfigs` | `startAfterProcess`; fires after mutations/documents/API calls/auth | | `references` | `TRecordReferencesConfig` | `initRecordReferences`; core record-reference lookups | | `tags` | `{ types: [{ type, description }] }` | core tags (serialized) | | `properties` | `IPropertyMeta` | core properties: `systemFields` per content type (serialized) | | `relations` | `{ subscribedTypes: string[] }` | core relations activity log (serialized) | | `logs` | `LogsConfigs` (`{ contentTypes: [{ moduleName, collectionName }] }`) | logs service (serialized) | Sales registers nearly all of these in [`sales_api/src/main.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/main.ts) and `src/meta/`; use it as the structural reference without importing its source. ## Permissions: `src/meta/permissions.ts` Declare an `IPermissionConfig` ([permission.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-types/modules/permissions/permission.ts)): `{ plugin, modules[], defaultGroups[] }`. Each module has `name`, `scopes` (`own`/`group`/`all`), `ownerFields`, `scopeField`, and `actions` (`{ title, name, description, always?, disabled?, oauthScope(s)?, type? }`). Resolvers enforce actions via `context.checkPermission('<action>')`; OAuth clients map to the same action names. `defaultGroups` provides ready-made groups keyed `<plugin>:<group>` (the colon marks them as default-group ids): ```ts // sales_api/src/meta/permissions.ts (excerpt) export const permissions: IPermissionConfig = { plugin: 'sales', modules: [ { name: 'deal', description: 'Deals management', ownerFields: ['userId', 'assignedUserIds'], scopes: [ { name: 'own', description: 'Deals created by or assigned to user' }, { name: 'all', description: 'All deals' }, ], actions: [ { title: 'View deals', name: 'showDeals', description: 'View deals', always: true }, { title: 'Add deals', name: 'dealsAdd', description: 'Create deals' }, ], }, ], defaultGroups: [ { id: 'sales:admin', name: 'Sales Admin', permissions: [{ plugin: 'sales', module: 'deal', actions: ['showDeals', 'dealsAdd'], scope: 'all' }], }, ], }; ``` ## `beforeResolvers` and `afterProcess` `beforeResolvers` ([beforeResolvers.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/beforeResolvers.ts)) intercepts GraphQL resolvers by name before they run: `{ resolvers: { <rootField>: [<resolverName>…] }, handler, check?, blocker? }`. The handler returns `{ status: 'ok' }` (optionally rewriting args), `{ status: 'blocked', code, message }`, or `{ status: 'resolved', data }`. Sales uses it to rewrite `productIds` on `productsRemove`/`productsMerge` to only the ids not referenced by deals. `afterProcess` fires after work completes: `{ rules: [{ type: 'afterMutation', mutationNames }], afterMutation, afterAuth, afterApiRequest, afterDocumentCreated, afterDocumentUpdated }`. Sales uses `afterMutation` to sync POS product groups and publish subscription events. ## Migrations and boundaries Keep migrations inside the plugin (for example `backend/plugins/operation_api/src/migrations/`). Never read or mutate another plugin's collections; communicate through published GraphQL, tRPC, HTTP, event, or federation interfaces. --- # Workers & Background Jobs BullMQ queues and delivery state Run deferred and scheduled work on BullMQ over the shared Redis connection instead of blocking requests. All queueing in plugins and `erxes-api-shared` is BullMQ on Redis; there is no RabbitMQ/amqp usage. For the standalone background services, see [Background Services](https://erxes.io/docs/architecture-background-services). ## The shared helpers [`erxes-api-shared/src/utils/mq-worker.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mq-worker.ts) wraps BullMQ: | Helper | What it does | | --- | --- | | `sendWorkerQueue(service, queueName)` | Returns a cached `Queue` named `<service>-<queueName>` on the shared `redis` connection. `add()` auto-serializes payloads (strips circular references) and applies `DEFAULT_JOB_OPTIONS` (`removeOnComplete: true`, `removeOnFail: { count: 5000, age: 24h }`). | | `createMQWorkerWithListeners(service, queueName, processor, redis, onReady, workerOptions?)` | Creates a `Worker` on the same `<service>-<queueName>` queue with `completed`/`failed`/`error`/`ready` logging; `workerOptions` carries concurrency, limiter, etc. | | `sendWorkerMessage({ pluginName, queueName, jobName, subdomain, data, defaultValue, timeout, options })` | Request/reply: adds a job to `<plugin>-<queue>`, waits on `QueueEvents` for the result, times out after 3000 ms by default; keep the consumer fast for synchronous calls. | The `redis` export (`utils/redis.ts`) is a shared `ioredis` client built from `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD`. Reuse it; queue names must match between producer and consumer, so always go through `sendWorkerQueue`/`createMQWorkerWithListeners` with the same `service` and `queueName`. ## The plugin pattern Plugins keep an `initMQWorkers(redis)` in `src/worker/index.ts` and call it from `onServerInit` in `startPlugin`. [`operation_api`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/worker/index.ts) is the active example: ```ts export const initMQWorkers = async (redis: any) => { const myQueue = new Queue('operations-daily-cycles-check', { connection: redis, defaultJobOptions: { removeOnComplete: true, removeOnFail: true }, }); // cron scheduler: BullMQ upsertJobScheduler await myQueue.upsertJobScheduler( 'operations-daily-cycles-check', { pattern: '0 * * * *', tz: 'UTC' }, { name: 'operations-daily-cycles-check' }, ); createMQWorkerWithListeners('operations', 'checkCycle', checkCycle, redis, onReady); createMQWorkerWithListeners('operations', 'daily-cycles-check', dailyCheckCycles, redis, onReady); }; // src/main.ts startPlugin({ name: 'operation', // … onServerInit: async () => { await initMQWorkers(redis); }, }); ``` The dispatcher job (scheduled) fans out per-tenant work onto a second queue: a job registered via `upsertJobScheduler` enqueues one job per organization via `sendWorkerQueue`. In `posclient_api` the `initMQWorkers(redis)` call happens in `main.ts`; in `content_api` the call exists but is commented out, so check the plugin's own `main.ts` before assuming its workers run. Two `meta` keys also start BullMQ workers for you (see [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions)): `payments` → `<plugin>-payments` and `importExport` → `<plugin>-import-processor` / `<plugin>-export-processor` (concurrency and limiter tunable via `IMPORT_EXPORT_IMPORT|EXPORT_CONCURRENCY`, `*_LIMITER_MAX`, `*_LIMITER_DURATION_MS`). ## Tenant isolation and delivery state A job payload carries `{ subdomain, data }`; the worker regenerates models per job with `generateModels(subdomain)`. Never reuse request-scoped models or cache them across jobs. `sendWorkerMessage` wraps the payload the same way. `content_api`'s Postiz delivery path ([`src/modules/cms/postiz/`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/worker.ts)) is the fullest delivery-state example, and it does not use BullMQ at all; `startCmsDeliveryWorker` runs a polling sweep that: - atomically claims a `CmsShares` job by setting `leaseUntil` (a 120 s lease) and incrementing `attempts`, - determines the delivery tenant from the job's `subdomain` (SaaS) or treats jobs as tenant-bound (self-hosted), - moves the record through a `state` machine (`PENDING`/`QUEUED` → delivered, `CANCELLED`, `UNKNOWN`), - backs off with `nextCheck = now + min(attempts * 30000, 300000)`, capping at 20 attempts. Follow that shape for new jobs: validate with Zod at the API boundary, persist a delivery record, claim work atomically, make retries idempotent, and show `state`/`attempts` in the UI (the `PostizDeliveryList` component does this for posts). ## Picking the right mechanism | Need | Mechanism | | --- | --- | | Fire-and-forget work off the request path | `sendWorkerQueue('<plugin>', '<queue>').add(jobName, { subdomain, ... })` plus a matching `createMQWorkerWithListeners` consumer | | Recurring work | `queue.upsertJobScheduler(schedulerId, { pattern, tz }, { name })` on a dedicated scheduler queue, fanning out per-tenant jobs | | Synchronous result from another service | `sendWorkerMessage` request/reply (3 s default timeout; keep the consumer fast) | | Platform-driven work | `meta.payments` / `meta.importExport` handlers; the shared lib starts the workers | | Long-running or externally paced delivery | Persisted delivery record + atomic lease + polling sweep (Postiz pattern), not a BullMQ `Worker` | ## How `ENABLED_SERVICES` relates Workers that live outside plugins run as separate Nx projects under `backend/services/`: `automations-service` and `logs-service`. `pnpm dev:apis` maps `ENABLED_SERVICES=automations,logs` to those project names and serves them next to your `<name>_api` projects. They run their own `initMQWorkers`: - `automations-service`: `automations-trigger`, `automations-action`, `automations-aiAgent` workers. Plugins enqueue onto `automations-*` queues via `sendWorkerQueue('automations', 'trigger')` (the same name the service consumes). - `logs-service`: event-log, activity-log, and segment workers, plus the after-process pipeline. Cross-service queue naming is the rule: `<service>-<queue>` producer and consumer must agree, so call `sendWorkerQueue` with the consuming service's name, not your plugin's. --- # API & Integrations Authenticate, query the federated GraphQL API, and connect external systems Call erxes from outside the platform: one federated GraphQL endpoint, a small set of credential types, and webhooks for event-driven integration. ## The endpoint All API traffic goes through the gateway on port `4000`: `POST /graphql` directly, or `https://<domain>/gateway/graphql` behind the reverse proxy from the deployment template. An Apollo Router inside the gateway ([apollo-router/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/apollo-router/index.ts)) composes the `core` subgraph plus one subgraph per enabled plugin API, so a single request can span modules and every plugin's operations share the endpoint. Subscriptions run over WebSocket on the same `/graphql` path, and plugin HTTP routes (webhooks, provider callbacks) are reachable through the gateway's `/pl:<name>` proxy. Never call a subgraph port directly: the gateway's [userMiddleware](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts) is what turns your credential into the context the subgraphs trust. See [Backend & API Gateway](https://erxes.io/docs/architecture-backend-gateway) for composition and proxying. ## Credential types The gateway checks every credential before routing, then forwards the result to subgraphs as `user`, `clientportal`, and `cpuser` headers. The common cases: | Credential | Transport | Issued by | Identifies | | --- | --- | --- | --- | | Team-member JWT | `Authorization: Bearer` or `auth-token` cookie | `login` mutation or WorkOS SSO; 1-day expiry, registered in Redis | A `Users` document | | Client Portal token | `x-app-token` header | `clientPortalAdd` / `clientPortalChangeToken`; JWT carrying `clientPortalId` | A `ClientPortals` record; scopes `cp*` operations | | App token | `erxes-app-token` header (`x-app-api-token` doubles as a CORS bypass) | [appsAdd](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/apps/graphql/schemas.ts) mutation (requires `appsManage`) | Machine principal `app:<id>`; owner when `allowAllPermission` | Issuance, rotation, and edge cases (expired tokens, dual headers, WebSocket auth) are in [Authentication & Permissions](https://erxes.io/docs/api-authentication). For a public website or portal frontend, use the portal token with `cp*` operations; see [Client Portal](https://erxes.io/docs/app-client-portal). > [!WARNING] > **tRPC is internal** > > `/<plugin>/trpc` exists on every service for service-to-service calls. It is not a supported external API; integrate over GraphQL or webhooks at the gateway. ## Guides - [Authentication & Permissions](https://erxes.io/docs/api-authentication): Team, portal, app, and agent credentials - [GraphQL API](https://erxes.io/docs/api-graphql): Federated endpoint and pagination - [Integration Development](https://erxes.io/docs/api-integration-development): External systems via GraphQL, tRPC, and webhooks --- # Authentication & Permissions Team, portal, app, and agent credentials How erxes identifies team members, portal visitors, and machine apps, and where each credential is checked. For the user model, see [Organizations, Users & Permissions](https://erxes.io/docs/core-organizations-users). For portal-specific flows, see [Client Portal](https://erxes.io/docs/app-client-portal). ## Credential types All of these are checked by [userMiddleware](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts) in `backend/gateway`, before the request reaches Apollo Router. | Credential | How issued | Transport | Identifies | Where checked | | --- | --- | --- | --- | --- | | Team-member session | `login` mutation (or WorkOS SSO); JWT `{ user: { _id, isOwner } }`, 1-day expiry, registered in Redis `user_token_<id>_<token>` for 24h | `Authorization: Bearer <jwt>` or `auth-token` httpOnly cookie | A `Users` document | Gateway: JWT signature + user lookup + Redis key must exist | | OAuth access token | `/oauth/token` (device-code flow) | Same as team member, JWT carries `typ: "oauth_access"`, `clientId`, `scope` | A user acting through an `OAuthClientApps` client | Gateway; scopes land on `req.user.oauthScopes` | | App token | `appsAdd` mutation (requires `appsManage`); random string on the `app_tokens` collection | `erxes-app-token` header | A machine principal `app:<id>`; `isOwner` when `allowAllPermission` | Gateway: `Apps.findOne({ token, status: "active" })`; 401 on miss | | CORS-bypass app token | Same `app_tokens` record | `x-app-api-token` header | Origin-check bypass only | Gateway CORS middleware; result cached in Redis 1h | | Client Portal token | `clientPortalAdd` / `clientPortalChangeToken`; JWT `{ clientPortalId }` signed with `JWT_TOKEN_SECRET`, no expiry | `x-app-token` header | A `ClientPortals` record | Gateway: JWT verify + portal lookup → `req.clientPortal` | | Portal-user session | `clientPortalUser*` auth mutations; JWT `{ userId, clientPortalId }`, 1d access / 7d refresh defaults | `client-auth-token` header or httpOnly cookie | A `CPUsers` record bound to that portal | Gateway, only after a valid `x-app-token` | | POS config token | Sales POS setup; `posChooseConfig` sets the cookie | `erxes-pos-token` header or `pos-config-token` cookie | A posclient `Configs` record | `posclient_api` config middleware | | POS user session | `posLogin` | `pos-auth-token` cookie | A posclient `PosUsers` record | `posclient_api` user middleware | | Website check | erxes-hosted verification | `erxes-core-token` + `erxes-core-website-url` headers | Synthetic user limited to `showIntegrations`, `showKnowledgeBase`, `showScripts` | Gateway: POST to `https://erxes.io/check-website` must answer `ok` | Check order in `userMiddleware`: website check → app token → client portal (`x-app-token`, then `client-auth-token`) → Bearer/`auth-token` user JWT. All credential types can ride the same request, for example an app token plus a portal token. ## Login example ```graphql mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) } ``` `login` returns the access token as a `String` and also sets the `auth-token` cookie (`httpOnly`; `secure`/`sameSite` depend on the request). Internally `Users.createTokens` ([Users.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/organization/team-member/db/models/Users.ts)) also mints a 7-day refresh JWT; `logout` clears the cookie and drops the Redis key, which revokes the token immediately rather than at JWT expiry. ```bash curl --fail https://<gateway>/graphql \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer <token>" \ --data '{"query":"query { currentUser { _id email } }"}' ``` ## SSO and OAuth - **WorkOS SSO (SaaS only)**: `loginWithGoogle` and `loginWithMagicLink` call `assertSaasEnvironment()` and build WorkOS authorization/session URLs using `WORKOS_API_KEY`, `WORKOS_PROJECT_ID`, and `CORE_DOMAIN`. Magic-link mail goes out through SendGrid. - **OAuth 2.0 device flow**: Core API mounts `/oauth/device/code`, `/oauth/device/approve`, `/oauth/device/deny`, `/oauth/token`, and `/oauth/revoke` ([oauth/routes.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/auth/routes/oauth/routes.ts); device-code grant with per-IP rate limits). Client apps are managed through `oauthClientAppsAdd`/`Edit`/`Revoke`/`Remove` with `public` or `confidential` types and per-client token lifetimes. ## What the gateway forwards Subgraphs never see your raw credentials. The middleware rewrites them into signed-by-position headers read by `erxes-api-shared` context builders: | Header | Contents | | --- | --- | | `user` | Base64 JSON of the compacted user document (`emailSignatures`, `customFieldsData`, `propertiesData`, `links`, `loginToken` stripped) | | `userid` | The user `_id` (also set for `app:<id>` principals) | | `clientportal` | Base64 JSON of the resolved portal | | `cpuser` | Base64 JSON of the portal user | | `sessioncode` | Passed through to `req.user.sessionCode` | > [!WARNING] > **Never trust these headers at the edge** > > `user`, `clientportal`, and `cpuser` are unsigned; they are trustworthy only because the gateway sets them. Expose only the gateway publicly: direct access to a subgraph port bypasses authentication. `JWT_TOKEN_SECRET` must be identical across gateway, Core API, and plugins, and must be set explicitly in production. For portal JWTs the shared code throws in `NODE_ENV=production` rather than falling back to the `'SECRET'` default. ## Permissions Team-member authorization runs through `checkPermission` / `wrapPermission` in `erxes-api-shared/core-modules/permissions`. Wrapped resolvers throw `Login required` (`UNAUTHORIZED`) with no `context.user`, then check the action list the plugin declares in `meta/permissions.ts` (`canGroup` → permission-group actions, `isOwner` bypass, `checkOAuthScope` for `oauth_access` tokens). Portal resolvers instead use `wrapPublicResolver` with `forClientPortal`/`cpUserRequired` flags, throwing `Client portal required` / `Client portal user required`. `cp*` operations and widget resolvers skip the team-member wrapper (`wrapperConfig.skipPermission`) and scope by portal/integration context; the `cp` prefix does not itself imply published-only or read-only data. ## Edge cases - Sending **two** `authorization` or `erxes-core-token` headers is a `400`, not a pick-one. - An expired, malformed, or Redis-evicted user token is **ignored**, not rejected: the request continues anonymous and wrapped resolvers fail with `Login required`. Only an unknown `erxes-app-token` gets an explicit `401`. - WebSocket subscriptions authenticate from the `auth-token` cookie on the upgrade request; `connectionParams` are not consulted. - Set `DEBUG_GATEWAY_AUTH=true` on the gateway to get structured JSON logs for every auth decision on `/graphql` (which credential was present, which check failed, token hash). --- # GraphQL API Federated endpoint and pagination Call the federated GraphQL endpoint, authenticate, paginate, and subscribe. For credentials, see [Authentication & Permissions](https://erxes.io/docs/api-authentication). ## Endpoint Every backend service (Core API on 3300, each plugin on 3305+) serves an Apollo Server v4 **subgraph** at its own `/graphql` using `@apollo/subgraph`. The gateway composes them into a supergraph with Rover ([supergraph-compose.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/apollo-router/supergraph-compose.ts)), spawns Apollo Router bound to `127.0.0.1:50000` (`APOLLO_ROUTER_PORT`), and proxies the public `/graphql` to it on port **4000** (`PORT`). Always call the gateway, never a subgraph port: the gateway's `userMiddleware` is what turns your credential into the `user`/`clientportal`/`cpuser` context headers subgraphs trust. The router config propagates **all** request headers to subgraphs (`headers.all.request.propagate.matching: ".*"`). ```bash curl --fail http://localhost:4000/graphql \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer <team-token>" \ --data '{"query":"query Health { __typename }"}' ``` A `200` with an `errors` array is still a failed operation: the router sets `include_subgraph_errors: all`, so resolver errors appear there. Check `errors` independently of HTTP status. Router and subgraph timeouts are 300s (`traffic_shaping`). ## Authentication headers | Header | Principal | | --- | --- | | `Authorization: Bearer <jwt>` / `auth-token` cookie | Team member | | `erxes-app-token` | Machine app (`app:<id>`) | | `x-app-token` (+ optional `client-auth-token`) | Client Portal (+ portal user) | Widget operations (`widgets*`, `cp*`, `helpCenterGetConfigByDomain`) skip the team-member wrapper and need only their own context; see [Authentication](https://erxes.io/docs/api-authentication) for the full check order. ## Pagination `erxes-api-shared` defines two parameter styles; each query declares one: - **Cursor** (`GQL_CURSOR_PARAM_DEFS`, [constants.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/constants.ts)): `limit`, `cursor`, `cursorMode` (`inclusive`/`exclusive`), `direction` (`forward`/`backward`), `orderBy` (JSON), `sortMode`, `aggregationPipeline` (`[JSON]`). Backed by `cursorPaginate`: `limit` defaults to 20 and must be 1–100; a cursor is base64 JSON of the item's sort fields plus `_id`. - **Offset** (`GQL_OFFSET_PARAM_DEFS`): `page`, `perPage`, `sortField`, `sortDirection`; used by a minority of legacy queries. Cursor-style lists return an envelope such as `DealsListResponse { list, totalCount, pageInfo }`; `PageInfo` is `{ hasNextPage, hasPreviousPage, startCursor, endCursor }` (from `commonTypeDefs.ts`). Feed `endCursor` back as `cursor`: ```graphql query Deals($cursor: String) { deals( pipelineId: "PIPELINE_ID" limit: 20 cursor: $cursor cursorMode: exclusive direction: forward orderBy: { createdAt: -1 } ) { list { _id name createdAt } totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } ``` First page: omit `cursor`. Next page: pass the previous `pageInfo.endCursor`. `backward` walks earlier pages; `hasPreviousPage`/`hasNextPage` flip accordingly. ## Subscriptions The gateway mounts a `graphql-ws` server (`graphql-transport-ws` protocol, [subscription/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/subscription/index.ts)) on the **same** HTTP server at path `/graphql`, so `ws://<gateway>:4000/graphql`. Connection auth reads the `auth-token` cookie from the upgrade request; each plugin registers its subscription resolvers over Redis pub/sub (`hasSubscriptions` in `startPlugin`). ```ts import { createClient } from 'graphql-ws'; const client = createClient({ url: 'wss://gateway.example.com/graphql' }); ``` Proxies in front of the gateway must allow WebSocket upgrade on `/graphql`. ## Introspection and limits - **Introspection**: enabled when `NODE_ENV=development` or `INTROSPECTION=true` in the gateway environment. In production it is off unless you opt in, and the gateway logs a warning if you do. - **Rate limiting**: a global limiter caps each IP at 5000 requests per 15 minutes (`/health` and `/bullmq-board` exempt). Core's `/initial-setup` route is separately capped at 100 per 15 minutes. - **Optional query armor**: setting `GRAPHQL_LIMITER` parses and validates every request body against the supergraph and enforces character limit 10,000, zero aliases, and depth ≤ 50; invalid documents get a `400` before reaching the router. ## Naming conventions Operation names are prefixed by their module (`cms*`, `sales*`, `cp*` portal-scoped, `clientPortal*`, `widgets*` public widget) and must be unique repo-wide. The `cp`/`widgets` prefixes mean "portal/widget context", not "read-only": enforce status filters in your queries. --- # Integration Development External systems via GraphQL, tRPC, and webhooks Connect an external system to erxes through GraphQL, HTTP webhooks, automation glue, or bulk import. For headers and principals, see [Authentication & Permissions](https://erxes.io/docs/api-authentication). For the endpoint itself, see [GraphQL API](https://erxes.io/docs/api-graphql). ## Choose an approach | You need | Use | Notes | | --- | --- | --- | | External system reads/writes erxes data | Federated GraphQL at `/graphql` with `erxes-app-token` | One supergraph across core and all enabled plugins | | erxes calls your system on events | **Outgoing webhook** automation action, or a plugin `expressRouter` | Retried HTTP with auth headers, configured in the workflow UI | | Your system pushes events into erxes | **Incoming webhook** automation trigger, or a plugin route (`/pl:<plugin>/<path>` via the gateway) | `POST /automation/<automationId>/<endpoint>` on the automations service | | Public website content/tickets | `cp*` operations + `x-app-token` portal token | See [Client Portal](https://erxes.io/docs/app-client-portal) | | Real-time updates | `graphql-ws` subscriptions on `wss://<gateway>/graphql` | Plugin-registered resolvers over Redis | | Bulk one-off data load | Import/Export module (`/import-export/download-template`, workers) | See [Import & Export](https://erxes.io/docs/core-import-export) | | Service-to-service inside the deployment | tRPC (`/<plugin>/trpc`) | Internal only; never expose or call across the public internet | ## GraphQL with an app token Create the token once (`appsAdd` needs `appsManage`), then send it on every call: ```bash curl --fail https://<gateway>/graphql \ -H 'Content-Type: application/json' \ -H 'erxes-app-token: <app-token>' \ --data '{"query":"mutation { customersAdd(firstName:\"Ada\", primaryEmail:\"ada@example.com\") { _id } }"}' ``` The gateway validates the token against the `app_tokens` collection (`status: "active"`), stamps `lastUsedAt`, and synthesizes `req.user = { _id: "app:<id>", isOwner: app.allowAllPermission === true }`. That satisfies `checkLogin`/`wrapPermission` resolvers; operations gated by `checkPermission` (such as `customersAdd`, which needs `contactsCreate`) additionally need `allowAllPermission` set on the app record, otherwise they fail `Permission required`. Revoking the app (`appsRevoke`/`appsRemove`) fails closed with `401 Invalid app token`. The same record in the `x-app-api-token` header bypasses the CORS origin list for server-side calls from arbitrary origins. ### End-to-end: create a customer and a deal ```graphql mutation CreateCustomer { customersAdd( firstName: "Ada" primaryEmail: "ada@example.com" state: "customer" ) { _id } } ``` ```graphql mutation CreateDeal($customerId: String!) { dealsAdd( name: "Inbound - Ada" customerIds: [$customerId] stageId: "STAGE_ID" status: "active" ) { _id name } } ``` `dealsAdd` is permission-gated (`dealsAdd`), so use a team-member token or an app record with `allowAllPermission`. Discover other operation names through introspection (dev or `INTROSPECTION=true`) or the plugin's `graphql/schemas/`. ## Webhooks Two directions exist, both centered on the automations service: - **Incoming**: an automation whose trigger type is `INCOMING_WEBHOOK` gets a URL of the form `POST|GET|… /automation/<automationId>/<config.endpoint>` on the automations service (default port 3302; reachable through the gateway's `/pl:` proxy when registered). The handler ([incomingWebhook.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/services/automations/src/executions/actions/webhook/incoming/incomingWebhook.ts)) matches on method + endpoint and keeps a 1 MB raw body for signature validation (`validateSecurity`). It applies per-webhook rate limits, validates the payload against the trigger's schema, and enqueues the execution. Failed lookups and security rejections are logged to the logs service as `webhook_security` events. A `/automation/<id>/health` probe and a `/<executionId>/<actionId>/continue/*` resume route for waiting executions are mounted alongside. - **Outgoing**: the `OUTGOING_WEBHOOK` action posts to a configured URL with templated headers/body, optional auth config, and retries with backoff on 408/425/429/5xx. Plugin HTTP endpoints also exist where a provider demands a fixed route: Frontline mounts `/facebook`, `/instagram`, `/mail/receive`, and `/callpro` routers (see [Email Delivery](https://erxes.io/docs/api-email)); other provider callbacks are part of the Enterprise Edition; `operation_api` mounts `POST /integrations/github/webhook` with signature verification in `githubWebhookHandler.ts`. Reach any of them through the gateway's `/pl:<plugin-name>/` proxy, which rewrites to the service root (for example `/pl:posclient/initial-setup`). > [!WARNING] > **tRPC is not a public API** > > `/<plugin>/trpc` exists on every service for service-to-service calls. It is not a supported external integration point; use GraphQL or webhooks at the gateway instead. ## Building your own inbound endpoint If you are writing a plugin that must receive provider webhooks, follow the `operation_api` GitHub integration (`modules/githubIntegration/` + [utils/githubWebhookHandler.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/utils/githubWebhookHandler.ts)): mount the route on the plugin's `expressRouter` in `startPlugin`, verify the provider signature against stored config, validate the payload, scope everything by `subdomain`, and make the handler idempotent; providers retry. --- # Frontline Inbox, channel integrations, tickets, forms, surveys, and knowledge base Customer service: unified inbox, channel integrations, tickets, forms, surveys, knowledge base, and help center configuration. Frontline is the customer-communication plugin. Agents work a unified inbox of conversations that arrive through per-channel integrations: erxes messenger, Facebook, Instagram, mail (Cloudflare Email Routing), Discord, Call (SIP), and Call Pro (webhook PBX). Conversations can be answered in place or converted into tickets (handled by Frontline's own ticket pipelines), deals (created by `sales` over tRPC), or tasks (created by `operation` over tRPC). The plugin also provides forms and form submissions, channel-scoped surveys, the knowledge base (topics → categories → articles), help center configuration records, response templates, and frontline reports. For embedding chat on a site, see [Messenger Widget](https://erxes.io/docs/erxes-messenger). For the published help center app, see [Help Center](https://erxes.io/docs/app-help-center). ## Enabling and ports ```dotenv ENABLED_PLUGINS=frontline ``` The base name maps to the Nx projects `frontline_api` and `frontline_ui`; the dev scripts append `_api`/`_ui` to each entry. | Project | Role | Dev port | | --- | --- | --- | | `frontline_api` | Federated GraphQL subgraph + tRPC + Express webhook routes; subscriptions enabled | `3304` | | `frontline_ui` | Module Federation remote (remote name `frontline_ui`) | `3004` | The API port is set in [`main.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/main.ts). ## Module map Backend modules live under `backend/plugins/frontline_api/src/modules/`. | Module | What it contains | Key collections | Module page | | --- | --- | --- | --- | | `inbox/` | Conversations, messages, integrations records, widget and client-portal resolvers, `receiveInboxMessage` | `conversations`, `conversation_messages`, `integrations` | [Inbox & Channels](https://erxes.io/docs/frontline-inbox-channels) | | `channel/` | Team and personal channels, membership, roles (`admin`/`lead`/`member`) | `channels`, `channel_members` | [Inbox & Channels](https://erxes.io/docs/frontline-inbox-channels) | | `response/` | Response templates | `response_templates` | [Inbox & Channels](https://erxes.io/docs/frontline-inbox-channels) | | `integrations/<kind>/` | `facebook`, `instagram`, `mail`, `discord`, `call`, `callpro` runtimes plus the `trpc` router | Per-kind mirror collections (`mail_integrations`, `conversations_discord`, `calls_cdr`, …) | [Channel Integrations](https://erxes.io/docs/frontline-integrations) | | `ticket/` | Pipelines, statuses, tickets, activities, notes, ticket configs, import/export | `frontline_tickets`, `frontline_tickets_pipeline`, `frontline_tickets_pipeline_status`, `frontline_ticket_activities`, `frontline_tickets_notes`, `frontline_ticket_configs` | [Tickets](https://erxes.io/docs/frontline-tickets) | | `form/` | Form definitions, fields, submissions | `frontline_forms`, `frontline_form_submissions` | [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys) | | `survey/` | Channel-scoped surveys, vote ledger, per-option ticket automation | `frontline_surveys`, `frontline_survey_votes` | [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys) | | `knowledgebase/` | Topics, categories, articles; AI knowledge source provider | `knowledgebase_topics`, `knowledgebase_categories`, `knowledgebase_articles` | [Knowledge Base](https://erxes.io/docs/frontline-knowledge-base) | | `helpcenter/` | The client portal config record behind a published help center site | `frontline_help_center_configs` | [Knowledge Base](https://erxes.io/docs/frontline-knowledge-base) | | `reports/` | Inbox/ticket/call/Facebook report aggregations, saved `ReportCharts` | `frontline_report_charts` | [Knowledge Base](https://erxes.io/docs/frontline-knowledge-base#reports) | The frontend remote under `frontend/plugins/frontline_ui/src/modules/` mirrors this structure (`inbox`, `ticket`, `pipelines`, `status`, `channels`, `forms`, `survey`, `knowledgebase`, `helpcenter`, `integrations`, `integrations-config`, `report`, `responseTemplate`, `settings`), plus `src/widgets/` automation remote entries and `src/pages/` route pages. Navigation is registered from `src/config.tsx` under `frontline/*` (`inbox`, `tickets`, `reports`, `calls/dashboard`, `calls/statistics`, `forms`, `surveys`, `knowledgebase`, `helpcenter`) and `settings/frontline/*` (`channels`, `personal-channel`, `config`). ## HTTP routes [`src/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/routes.ts) mounts webhook routers on the plugin's Express app: `/facebook`, `/instagram`, `/mail` (`POST /mail/receive`, HMAC-signed by the Cloudflare worker), and `/callpro` (`POST /callpro/receive`, only when `CALLPRO_ENABLED=true`). See [Channel Integrations](https://erxes.io/docs/frontline-integrations). ## tRPC [`src/init-trpc.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/init-trpc.ts) merges the `integration`, `inbox`, `conversation`, `form`, `ticket`, `relation`, and `fields` routers. Notable procedures include `inbox.createConversationAndMessage`, `inbox.getConversationsList`, `ticket.create`, `form.submissionsByConversation`, and `relation.onRelationAdded`. ## How it connects to Core - Contacts/customers come from Core: inbound messages and webhooks create or reuse core customers over tRPC. - `meta/` registers permissions (`inbox`, `channel`, `integration`, `ticket`, `form`, `survey`, `callReport`, `responseTemplate`, `knowledgeBase`, `helpCenter` modules), automations (facebook, instagram, inbox, tickets, discord workers plus the knowledgebase AI source), notifications, segments, references, `relations` (`frontline:conversation` subscribed type), tags (`conversation`, `ticket`, `form`), and `properties` (the `conversation` and `ticket` property types, each with `systemFields`). - Import/export producers cover ticket import/export (`frontline:ticket.ticket`) and form-submission export (`frontline:formSubmission.formSubmission`). - Messenger and form embed bundles are built by the `apps/frontline-widgets` app (`messengerBundle.js`, `formBundle.js`); install dialogs build snippet URLs from `REACT_APP_WIDGETS_URL`. ## Configuration Backend env vars read by this plugin: | Variable | Purpose | | --- | --- | | `CALLPRO_ENABLED` | `true` turns on Call Pro | | `CALLPRO_SUBDOMAINS` | Comma-separated tenant list that restricts Call Pro on shared deployments | | `MAIL_RECEIVE_URL`, `MAIL_WORKER_URL`, `MAIL_TENANT`, `MAIL_WEBHOOK_SECRET` | Inbound mail delivery from the Cloudflare worker | | `MAIL_DOMAIN`, `MAIL_SENDING_ACCOUNT_ID`, `MAIL_SENDING_API_TOKEN`, `MAIL_SENDING_DAILY_LIMIT` | The deployment's own Cloudflare account used when a workspace has not connected its own | | `CALL_ENDPOINT_URL`, `CALL_WEBHOOK_REQUIRE_SIGNATURE`, `CALL_SUBSCRIPTION_REQUIRE_AUTH` | Call (SIP) integration | | `FACEBOOK_GRAPH_URL` | Graph API base URL | | `DOMAIN`, `ENDPOINT_URL`, `JWT_TOKEN_SECRET`, `VERSION` | Shared platform vars (`VERSION=saas` toggles SaaS behavior) | Meta app credentials (`FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET`, `FACEBOOK_VERIFY_TOKEN`, `FACEBOOK_POST_APP_ID`, `FACEBOOK_POST_APP_SECRET`, `INSTAGRAM_*`) are read through `getConfig`: per-workspace records first, env vars as fallback, env-only when `VERSION=saas`. See [Channel Integrations](https://erxes.io/docs/frontline-integrations). ## Pages in this section - [Inbox & Channels](https://erxes.io/docs/frontline-inbox-channels): Conversations, messages, team and personal channels, and response templates - [Channel Integrations](https://erxes.io/docs/frontline-integrations): Facebook, Instagram, mail, Discord, call, and Call Pro runtimes and webhooks - [Tickets](https://erxes.io/docs/frontline-tickets): Boards, pipelines, statuses, and conversation-to-ticket conversion - [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys): Form definitions, submissions, lead capture, and in-conversation surveys - [Knowledge Base](https://erxes.io/docs/frontline-knowledge-base): Topics, categories, articles, and help center configuration - [Messenger Widget](https://erxes.io/docs/erxes-messenger): Embed the Frontline Messenger widget on your website and verify the conversation round trip - [Help Center](https://erxes.io/docs/app-help-center): Public knowledge base and ticket flows --- # Inbox & Channels Conversations, messages, team and personal channels, and response templates Work the unified inbox: conversations and messages scoped by channel membership, team and personal channels, response templates, and conversation-to-card conversion. Backend modules: `backend/plugins/frontline_api/src/modules/{inbox,channel,response}`. Frontend: `frontend/plugins/frontline_ui/src/modules/{inbox,channels,responseTemplate}` and the `/frontline/inbox` route. ## Concepts ### Conversations and messages A `Conversation` belongs to one `Integration` and one customer, carries `status` (`new`, `open`, `closed`, `engageVisitorAuto`), `assignedUserId`, `participatedUserIds`, `readUserIds`, `tagIds`, `hasSurvey`, and `automatedReplyControl` (`status`, `reason`, `pausedUntil`). A `ConversationMessage` carries `content`, `attachments`, `internal` (agent-only note), `fromBot`, `botData`, `mailData` for the mail channel, and `extraData` (survey snapshots, Discord polls). Customer messages land through `receiveInboxMessage` (`modules/inbox/receiveMessage.ts`), which creates the conversation and message, then publishes `conversationClientMessageInserted` to every member of the conversation's channel. One GraphQL subscription covers all of a user's channels. ### Membership-scoped visibility An agent only sees conversations whose integration sits on a channel they belong to. `Builder` in [`src/conversationQueryBuilder.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/conversationQueryBuilder.ts) maps the caller's `ChannelMembers` rows to integration ids and intersects them with `channelId`, `brandId`, and `integrationId` filters. A `system`-role user bypasses the membership filter. Channels the user does not belong to return an empty `integrationId.$in`; there is no error, just no rows. ### Team vs personal channels | | Team channel | Personal channel | | --- | --- | --- | | `scope` | `team` (default; missing `scope` on legacy records means `team`) | `personal` | | Members | Many, via `channelAddMembers` | Exactly one: the owner, as `admin` | | Provisioning | `channelAdd` | Lazy: created the first time `getPersonalChannel` runs or an integration is created without a `channelId` | | Integrations | Any kind | Any kind; `integrationsCreateExternalIntegration` without `channelId` attaches to the caller's personal channel | Members carry a `role` of `admin`, `lead`, or `member` (`ChannelMembers`, unique on `(channelId, memberId)`). A partial unique index on `Channels` allows at most one personal channel per `createdBy`, and removing or demoting the last `admin` of a channel is refused. ### Response templates Per-channel canned replies (`ResponseTemplates`: `name`, `content`, `channelId`, `files`). `conversationMessageAdd` accepts a `responseTemplateId` to send one. ## Conversation filters and counts Every conversation list/count query accepts the same filter params: `channelId`, `integrationId`, `integrationType`, `status`, `brandId`, `tag`, `customerId`, `segment`, `searchValue`, `startDate`/`endDate`, and the string flags `unassigned`, `participating`, `mentioned`, `unread`, `starred`, `awaitingResponse`, `withSurvey`, `automationStatus`. - `withSurvey: "true"` keeps only conversations carrying a survey (`hasSurvey`). An `integrationType`-scoped list **without** `withSurvey` excludes them instead, so the inbox's `Messenger` row and `Surveys` row are disjoint. - `automationStatus` maps `standby` → `handoff_requested` and `handoff` → `human_active` on `automatedReplyControl.status`; `responded` matches any conversation automation touched. - `awaitingResponse: "true"` filters to `isCustomerRespondedLast: true`. - `searchValue` matches customers via Core (name/email/phone; ≥4 digits triggers phone search, which also scans `CallCdrs` by `src`/`dst`) plus conversation `content`. Two builders exist. The Mongo `Builder` (`src/conversationQueryBuilder.ts`) backs `conversations`, `conversationsTotalCount`, `conversationsGetLast`, `conversationsTotalUnreadCount`, and the scalar buckets of `conversationCounts`. The Elasticsearch-backed `CommonBuilder` (`modules/inbox/conversationUtils.ts`) serves the `conversationCounts(only: "byChannels"|"byIntegrationTypes"|"byTags"|"byIntegrations")` buckets. `CommonBuilder.runQueries` currently returns `0` (the `conversations` Elasticsearch index is not populated), so the sidebar reads per-kind counts from `integrationsGetUsedTypesByChannel` instead. ## Queries and mutations Schema: [`modules/inbox/graphql/schemas/conversation.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/inbox/graphql/schemas/conversation.ts). | Operation | Kind | Purpose | | --- | --- | --- | | `conversations(filter…)` | query | Cursor-paginated inbox list | | `conversationDetail(_id)` / `conversationMessages(conversationId, …)` | query | Detail and message history | | `conversationCounts(only, …)` / `conversationsTotalCount` / `conversationsTotalUnreadCount` | query | Sidebar counts | | `conversationMessageAdd(conversationId, content, internal, attachments, poll, replyToMessageId, …)` | mutation | Agent reply, internal note, or native poll | | `conversationMessageEdit`, `conversationMarkAsRead` | mutation | Edit / mark read | | `conversationsAssign` / `conversationsUnassign` / `conversationsChangeStatus` / `conversationsResolve` | mutation | Triage actions | | `conversationSetAutomatedReplyControl(_id, status, reason, pausedUntil)` | mutation | Pause/hand off automation on a conversation | | `conversationConvertToCard(_id, type, itemName, stageId, …)` | mutation | Convert to `ticket`, `deal`, or `task` | | `conversationConvertedItems(_id)` | query | What a conversation was already converted into | | `getMyChannels` / `getChannels` / `getChannel(_id)` / `getPersonalChannel` / `getChannelMembers` | query | Channel reads | | `channelAdd` / `channelUpdate` / `channelRemove` / `channelAddMembers` / `channelRemoveMember(s)` / `channelUpdateMember` | mutation | Channel management | | `responseTemplates(filter)` / `responseTemplatesAdd` / `responseTemplatesEdit` / `responseTemplatesRemove` | query/mutation | Response templates | | `integrations` / `integrationsGetUsedTypes` / `integrationsGetUsedTypesByChannel` / `integrationDetail` | query | Integration list and sidebar kinds | | `conversationClientMessageInserted(userId)` | subscription | Live customer messages for the whole inbox | ## Convert a conversation to a ticket, deal, or task `conversationConvertToCard(_id, type, …)` dispatches to a per-kind handler in `modules/inbox/services/conversationConvertTargets.ts`: | `type` | Created by | Permission checked | URL returned by `conversationConvertedItems` | | --- | --- | --- | --- | | `ticket` | `Ticket.addTicket` in this plugin | `createTicket` + pipeline access | `/frontline/tickets?ticketId=<id>` | | `deal` | `sales` `deal.createItem` over tRPC | `dealsAdd` | `/sales/deals?boardId=…&pipelineId=…&salesItemId=<id>` | | `task` | `operation` `task.createFromSource` over tRPC | `taskCreate` | `/operation/tasks/<id>` | The new item is related to the conversation (and its customer for tickets/tasks) through Core relations. A second item of the same kind for one conversation is refused; `stageId` is required (a status id for tickets/tasks, a stage id for deals). ```graphql mutation { conversationConvertToCard( _id: "conv_abc123" type: "ticket" itemName: "Billing issue from messenger" stageId: "status_xyz" assignedUserIds: ["user_1"] tagIds: ["tag_9"] ) } ``` ## Widget and client portal resolvers The unauthenticated messenger widget talks to the `widgets*` operations (`widgetsMessengerConnect`, `widgetsInsertMessage`, `widgetsConversations`, `widgetsConversationDetail`, `widgetsMessages`, `widgetsTotalUnreadCount`, `widgetsMessengerSupporters`, `widgetsGetEngageMessage`, `widgetsSendTypingInfo`, `widgetsReadConversationMessages`) and the ticket-widget operations (`widgetTicketCreated`, `widgetTicketCheckProgress`, `widgetTicketComments`, `widgetTicketActivityLogs`, `widgetTicketsByCustomer`). Signed-in portal users use the `cp*` conversation operations (`cpConnect`, `cpConversations`, `cpInsertMessage`, `cpReadConversationMessages`). For installation, see [Messenger Widget](https://erxes.io/docs/erxes-messenger). ## tRPC procedures `inbox.*` router highlights ([`src/modules/inbox/trpc/inbox.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/inbox/trpc/inbox.ts)): | Procedure | Kind | Purpose | | --- | --- | --- | | `inbox.createConversationAndMessage` | mutation | Create a conversation and its first message (used by survey submit and other services) | | `inbox.createOnlyMessage` | mutation | Append a message to an existing conversation | | `inbox.integrations.receive` | mutation | Inbound webhook entrypoint → `receiveInboxMessage` | | `inbox.integrations.remove` | mutation | Remove an integration plus its conversations and messages | | `inbox.conversationClientMessageInserted` | mutation | Publish the live-message subscription event | | `inbox.getConversationsList` | query | Cursor-paginated conversation list | | `inbox.conversations.find` / `findOne` / `count` | query | Conversation reads | | `inbox.conversations.changeStatus` | query | Set `status` (`new`/`open`/`closed`/`resolved`) | | `inbox.conversationMessages.find` / `findOne` | query | Message reads | | `inbox.integrations.find` / `findOne` / `count` | query | Integration reads | | `inbox.getIntegrationKinds` | query | Kind → label map (`messenger`, `lead`, `webhook`, `mail`, `facebook-messenger`, …) | | `inbox.channels.find`, `inbox.getConversations`, `inbox.removeCustomersConversations`, `inbox.changeCustomer`, `inbox.updateUserChannels`, `inbox.sendNotifications`, `inbox.widgetsGetUnreadMessagesCount` | query/mutation | Service-to-service helpers | Also merged into the root router: `conversation.find`/`conversation.tag`, `relation.onRelationAdded` (logs a conversation's form submissions onto a related ticket or other entity), and `integration.find`. ## Permissions Permission modules: `inbox` (`showConversations` always granted, `conversationMessageAdd`, `conversationMessageEdit`, `conversationsAssign`, `conversationsUnassign`, `conversationsChangeStatus`, `conversationsResolve`, `conversationConvertToCard`, `conversationEditCustomFields`), `channel` (`showChannels`, `showAllChannels`, `channelAdd`, `channelUpdate`, `channelRemove`, `channelManageMembers`), `integration` (`showIntegrations`, `integrationsAdd`, `integrationsEdit`, `integrationsRemove`), and `responseTemplate` (`showResponseTemplates`, `responseTemplatesAdd`, `responseTemplatesEdit`, `responseTemplatesRemove`). Default groups: `frontline:admin`, `frontline:user`, `frontline:viewer`. ## Automations, segments, notifications - Automation triggers: `frontline:inbox.conversations` "Conversation event" (assignee/status/tag changes) and "Erxes Messenger Message" (direct message, Get Started, quick reply, request-create-ticket, ticket-form-submission conditions); action "Send Erxes Messenger Message". - Segment content types and field definitions are contributed for conversations and messages (`modules/inbox/meta/segments/`). - Notification events include `conversationAddMessage`, `conversationAssigneeChange`, `conversationCreated`, `conversationParticipantAdded`, `conversationStateChange`, `conversationTagged`. - `meta/afterProcess` handlers maintain unread counters and channel counts after conversation changes. --- # Channel Integrations Facebook, Instagram, mail, Discord, call, and Call Pro runtimes and webhooks Connect external channels to the inbox: Facebook, Instagram, mail, Discord, Call, and Call Pro, plus the integration lifecycle and webhook routes. Backend: `backend/plugins/frontline_api/src/modules/integrations/<kind>/` holds one runtime per kind; `integrations/trpc` is the in-plugin tRPC router. Frontend: `frontend/plugins/frontline_ui/src/modules/{integrations,integrations-config}`. ## Concepts ### Integration kinds Every `Integration` record has a `kind`. `inbox.getIntegrationKinds` (`modules/inbox/utils.ts`) returns the registered map: `messenger` (Messenger), `lead` (Popups & forms), `webhook` (Webhook, a generic inbound webhook with an auto-generated `token`), `booking`, `callpro` (Callpro), `mail` (Email), `facebook-messenger`, `facebook-post`, `instagram-messenger`, `instagram-post`, `discord-messenger` (Discord), `calls` (Phone call), `client` (Client Portal), and `vendor` (Vendor Portal). Creation code also accepts `cloudflarecalls` as a service kind. An integration is always attached to one channel. `integrationsCreateExternalIntegration` refuses `channelId` values that point at another user's personal channel; when no `channelId` is given, the integration lands in the connecting user's own personal channel. > [!WARNING] > `integrationsCreateExternalIntegration` fails with `Channel … not found` for an unknown `channelId` (create the channel first) and with `Cannot create an integration on another user's personal channel` when the target is someone else's `scope: personal` channel (omit `channelId` to use your own). ### Lifecycle operations Mutations live in [`resolvers/mutations/integrations.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/inbox/graphql/resolvers/mutations/integrations.ts). | Operation | Purpose | | --- | --- | | `integrationsCreateExternalIntegration(data, channelId, brandId, accountId, kind, name, …)` | Create a channel integration; forwards to the per-kind service over tRPC | | `integrationsCreateMessengerOnboarding` / `integrationsEditMessengerOnboarding` / `integrationsCreateMessengerIntegration` / `integrationsEditMessengerIntegration` / `integrationsSaveMessengerConfigs` / `integrationsSaveMessengerAppearanceData` / `integrationsSaveMessengerColorTheme` / `integrationsSaveMessengerTicketData` | erxes messenger integration + widget config | | `integrationsCreateLeadIntegration` / `integrationsEditLeadIntegration` / `integrationsCopyLeadIntegration` | Lead (form) integrations | | `integrationsEditCommonFields(_id, name, details, channelId, brandId)` | Rename, re-channel, or update `details`; pushes `sendUpdateIntegration` to the kind's service | | `integrationsRepair(_id, kind)` | Recreate the service-side resources for an integration | | `integrationsArchive(_id, status)` | Archive/unarchive a conversation stream | | `integrationsRemove(_id)` | Remove the integration and its conversations/messages | | `integrationsRemoveAccount(_id, kind)` | Remove an OAuth account and all its integrations | | `integrationsSendSms(integrationId, content, to)` | Send an SMS through a supported integration | Queries: `integrations`, `integrationDetail`, `allLeadIntegrations`, `integrationsGetUsedTypes`, `integrationsGetUsedTypesByChannel`, `integrationGetLineWebhookUrl`, `integrationsTotalCount`, and `integrationsGetMessengerColorThemes`, plus per-kind lists. ## Webhook routes [`src/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/routes.ts) mounts routers on the plugin's Express app: | Mount | Purpose | | --- | --- | | `/facebook` | Meta webhook: `GET` verification against `FACEBOOK_VERIFY_TOKEN` config, `POST` Messenger/feed/comment events | | `/instagram` | Meta webhook for Instagram events | | `/mail` | `POST /mail/receive`: HMAC-signed email payloads from the Cloudflare Email Routing worker | | `/callpro` | `POST /callpro/receive`: Call Pro PBX events; mounted only when `CALLPRO_ENABLED=true` | Inbound payloads flow into `receiveInboxMessage`, which creates/reuses the customer, conversation, and message and publishes `conversationClientMessageInserted`. ## Facebook and Instagram (Meta) App credentials come from `getConfig`: per-workspace config records first, environment variables as fallback; environment-only when `VERSION=saas`. | Env var | Purpose | | --- | --- | | `FACEBOOK_APP_ID`, `FACEBOOK_APP_SECRET` | Main Meta app for messenger/post integrations | | `FACEBOOK_POST_APP_ID`, `FACEBOOK_POST_APP_SECRET` | Separate app for comment/feed captures | | `FACEBOOK_VERIFY_TOKEN` | Webhook `GET` verification token | | `FACEBOOK_PERMISSIONS` | OAuth permission list | | `FACEBOOK_LOGIN_REDIRECT_URL` | OAuth redirect URL | | `FACEBOOK_GRAPH_URL` | Graph API base URL | | `INSTAGRAM_APP_ID`, `INSTAGRAM_APP_SECRET`, `INSTAGRAM_PERMISSIONS`, `INSTAGRAM_LOGIN_REDIRECT_URL`, `INSTAGRAM_VERIFY_TOKEN` | Instagram equivalents | The `facebook`/`instagram` modules store per-integration tokens and page/account state, mirror comments/messages into conversations, and expose GraphQL login/reply/account helpers. Facebook and Instagram automation triggers/actions (e.g., comment events) are registered from `meta/automations`, and the Facebook comment outbox worker starts at plugin init (`main.ts`). ## Mail Inbound and outbound mail run through Cloudflare. 1. A Cloudflare Email Routing worker posts inbound email to `POST /mail/receive` on this API, signed with `MAIL_WEBHOOK_SECRET` (HMAC). `MAIL_RECEIVE_URL`, `MAIL_WORKER_URL`, and `MAIL_TENANT` configure the worker side. 2. Mail messages thread onto conversations by `mailData`/`References`/`In-Reply-To` headers; each mail integration maps to one inbox channel. 3. Outbound mail uses Cloudflare Email Sending. A workspace connects its own Cloudflare account (`mailCloudflareConnect`, `mailCloudflareProvision`, `mailCloudflareDisconnect`); without one, the deployment's fallback account (`MAIL_DOMAIN`, `MAIL_SENDING_ACCOUNT_ID`, `MAIL_SENDING_API_TOKEN`, `MAIL_SENDING_DAILY_LIMIT`) is used. 4. `mailPipelineIntegration`/`mailPipelineConnect`/`mailPipelineUpdate`/`mailPipelineForwardVerified`/`mailPipelineDisconnect` route mail into pipelines; `mailSendMail`, `mailMessageRetry`, `mailConversationDetail`, `mailSendingReadiness`, `mailCloudflareConnection`, `mailCloudflareSendingQuota`, `mailCloudflareZones`, `mailCheckConnection` cover sending, retry, and diagnostics. ## Discord `initDiscord()` runs at plugin startup (`main.ts`) and opens a Discord gateway client per configured integration; guild messages become conversations and agent replies post back through the Discord API. Discord automation entries are registered in `meta/automations`. ## Call (SIP/CDR) The `call` module stores `CallCdrs` and integrates a SIP provider via `CALL_ENDPOINT_URL`. `CALL_WEBHOOK_REQUIRE_SIGNATURE=true` enforces signed webhooks; `CALL_SUBSCRIPTION_REQUIRE_AUTH=true` requires auth on the call GraphQL subscription. CDR records join inbox search by phone number and feed the call report services. ## Call Pro (webhook PBX) Call Pro is gated per tenant (the `subdomain` a request belongs to): - `CALLPRO_ENABLED=true` turns the module on; `CALLPRO_SUBDOMAINS` is a comma-separated tenant allowlist on shared deployments. - Tenants outside the allowlist get a disabled config, a `404` from `/callpro/receive`, and rejected create/update operations. - `POST /callpro/receive` ingests PBX call events, which appear as conversations/CDR entries and feed call reports. ## Bot automations Conversation automation is coordinated through `automatedReplyControl` on each conversation (`status`, `reason`, `pausedUntil`) and the `conversationSetAutomatedReplyControl` mutation. A bot sets `handoff_requested`; an agent accepts with `human_active`. The `automationStatus` inbox filter exposes these states (`standby`, `handoff`, `responded`). ## Environment variables | Variable | Module | Purpose | | --- | --- | --- | | `FACEBOOK_*` / `INSTAGRAM_*` | facebook, instagram | Meta app credentials and OAuth settings | | `FACEBOOK_GRAPH_URL` | facebook | Graph API base URL | | `MAIL_RECEIVE_URL`, `MAIL_WORKER_URL`, `MAIL_TENANT`, `MAIL_WEBHOOK_SECRET` | mail | Inbound worker delivery | | `MAIL_DOMAIN`, `MAIL_SENDING_ACCOUNT_ID`, `MAIL_SENDING_API_TOKEN`, `MAIL_SENDING_DAILY_LIMIT` | mail | Deployment-level Cloudflare Email Sending fallback | | `CALL_ENDPOINT_URL`, `CALL_WEBHOOK_REQUIRE_SIGNATURE`, `CALL_SUBSCRIPTION_REQUIRE_AUTH` | call | SIP/CDR endpoint and auth toggles | | `CALLPRO_ENABLED`, `CALLPRO_SUBDOMAINS` | callpro | Enable + tenant allowlist | | `DOMAIN`, `ENDPOINT_URL`, `JWT_TOKEN_SECRET`, `VERSION` | shared | Platform URLs, token signing, `saas` mode | --- # Tickets Boards, pipelines, statuses, and conversation-to-ticket conversion Manage pipelines, statuses, tickets, activities, notes, and per-workspace ticket configs, including survey-driven ticket automation and client portal access. Code lives in [`frontline_api/src/modules/ticket`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/ticket) (backend) and [`frontline_ui/src/modules/{ticket,pipelines,status}`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/frontline_ui/src/modules) (frontend, served at `/frontline/tickets`). ## Concepts ### Pipelines A `Pipeline` is a named ticket workflow bound to a channel (`channelId`) with `memberIds`, `visibility` (public/private), optional `branchIds`/`departmentIds`, and ordering/numbering config (`isCheckDate`, `isCheckUser`, `isCheckDepartment`, `isCheckBranch`, `isHideName`, `excludeCheckUserIds`, `propertyIds`, name/number fields). `propertyIds` selects which properties appear on tickets in the pipeline. The `ticket` property type is registered through `meta` with `systemFields`. Private pipelines require membership; `isCheckUser`/`isCheckBranch`/`isCheckDepartment`/`isCheckDate` further restrict which tickets a member sees. ### Statuses A `Status` belongs to a pipeline and carries `visibilityType`, `memberIds`, `canEditMemberIds`, `canMoveMemberIds`, and `type`. Status visibility feeds `PermissionValidator.getHiddenStatusIds`: a private status whose `memberIds` excludes you hides its tickets from lists, and `canEditMemberIds`/`canMoveMemberIds` gate edits and status moves per user. ### Ticket records A `Ticket` carries `name`, `description`, `pipelineId`, `statusId`, `priority`, `assigneeId`, `assignedMembers`, `labelIds`, `tagIds`, `branchId`, `departmentId`, `channelId`, `number`, `state` (`active`, `archived`, `deleted`), `propertiesData`, `attachments`, `companyId`/`customerIds`, dates, and `sourceSurvey` (the survey snapshot written by ticket automation). Filters on `getTickets` include search, status/pipeline/channel, assignee, ownership, and `state`. ### Activities, notes, configs - `Activity` rows record ticket events; `getTicketActivities` lists them. - `Note` rows are user-authored ticket notes: `ticketGetNote`, `ticketGetNotes`, `ticketCreateNote`, `ticketUpdateNote`, `ticketDeleteNote`, plus client portal equivalents. - `TicketConfig` records hold per-pipeline/per-ticket settings via `ticketConfigs`, `ticketConfigDetail`, `ticketConfig`, `ticketSaveConfig`, `ticketRemoveConfig`. ## Queries and mutations Schema: [`modules/ticket/graphql/schemas/ticket.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/ticket/graphql/schemas/ticket.ts). | Operation | Kind | Purpose | | --- | --- | --- | | `getTickets` / `getTicket(_id)` | query | Filtered list / detail | | `getTicketPipelines` / `getTicketPipeline(_id)` | query | Pipeline reads | | `getTicketStatus(_id)` / `getAccessibleTicketStatuses` / `getTicketStatusesChoicesPipeline` / `getTicketStatusesByType` | query | Status reads respecting visibility | | `getTicketActivities` / `ticketGetNote` / `ticketGetNotes` | query | Activity and note reads | | `ticketConfigs` / `ticketConfigDetail` / `ticketConfig` | query | Config reads | | `createTicket` / `updateTicket` / `removeTicket` | mutation | Ticket writes | | `createPipeline` / `updatePipeline` / `removePipeline` | mutation | Pipeline management | | `addTicketStatus` / `updateTicketStatus` / `deleteTicketStatus` | mutation | Status management | | `ticketCreateNote` / `ticketUpdateNote` / `ticketDeleteNote` | mutation | Note writes | | `ticketSaveConfig` / `ticketRemoveConfig` | mutation | Config writes | | `cpGetTickets` / `cpGetTicket` / `cpGetTicketTotalCount` / `cpGetTicketStatus` / `cpTicketGetNotes` | query | Client portal reads | | `cpCreateTicket` / `cpUpdateTicket` / `cpTicketCreateNote` | mutation | Client portal writes | The widget also exposes `widgetTicketCreated`, `widgetTicketCheckProgress`, `widgetTicketCheckProgressForget`, `widgetTicketCommentAdd`, `widgetTicketCommentRemove`, and `widgetTicketsByCustomer` for embedded ticket widgets (see [Inbox & Channels](https://erxes.io/docs/frontline-inbox-channels)). ```graphql mutation { createTicket( name: "Follow-up: billing dispute" pipelineId: "pipe_1" statusId: "status_1" assigneeId: "user_1" channelId: "chan_1" ) { _id number } } ``` ## Survey-driven ticket automation Survey options can arm ticket creation (`ticketCreationEnabled`, `ticketCreationThreshold`, `ticketPipelineId`, `ticketStatusId`). When a vote lands, `runSurveyTicketAutomation` ([`modules/survey/ticketAutomation.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/survey/ticketAutomation.ts)) counts `SurveyVotes` for the option, atomically claims it (`ticketClaimedAt`, 5-minute stale-claim window), and creates exactly one ticket named `"<survey title> — <option text>"` with a `sourceSurvey` snapshot. See [Forms & Surveys](https://erxes.io/docs/frontline-forms-surveys). ## Import and export `main.ts` registers import/export producers for content type `frontline:ticket.ticket` (permissions `ticketsImportManage` / `ticketsExportManage`). Form submissions export separately under `frontline:formSubmission.formSubmission` (`formSubmissionsExportManage`). ## tRPC - `ticket.create`: service-side ticket creation (`doc` + `userId`), used by the survey automation and inbox conversion. - `relation.onRelationAdded`: when a relation is added to a conversation, its form submissions are logged onto the related entity. ## Permissions The `ticket` permission module covers `showTickets` (always granted), `createTicket`, `updateTicket`, `removeTicket`, `ticketStatusesManage`, `ticketsImportManage`, and `ticketsExportManage`. There are no pipeline-level permission actions. Pipeline access is enforced by `visibility`/`memberIds` and `PermissionValidator` ([`modules/ticket/utils/permissionValidator.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/ticket/utils/permissionValidator.ts)), which also applies per-status view/edit/move gates on top of those. > [!WARNING] > Edits and status moves fail with `PermissionError` when the status's `canEditMemberIds`/`canMoveMemberIds` excludes the caller. --- # Forms & Surveys Form definitions, submissions, lead capture, and in-conversation surveys Capture leads with embeddable forms and run channel-scoped surveys inside Messenger conversations, including one-vote-per-user enforcement and survey-driven ticket automation. Backend modules: `backend/plugins/frontline_api/src/modules/{form,survey}`. Frontend: `frontend/plugins/frontline_ui/src/modules/{forms,survey}` at `/frontline/forms` and `/frontline/surveys`. ## Forms ### Definitions and fields A `Form` carries `name`, `title`, `code`, `type`, `description`, `buttonText`, `numberOfPages`, `visibility`, `leadData`, `languageCode`, `departmentIds`, `tagIds`, `channelId`, and `integrationId`. Its fields are `FrontlineField` records managed through the `frontlineFields*` operations (each with `validation`, `regexValidation`, `validator`, `options`, `order`, `isRequired`, `logics`, and `subFields`). Field validation is defined by a `FieldValidator` (`type` of `PRESET`/`CUSTOM`/`NONE`, `presetKey`, `customRegex`, `errorMessage`) backed by [`form/validationRegistry.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/form/validationRegistry.ts). Preset keys: `EMAIL`, `NUMBER`, `DATE_TIME`, `DATE`, `PHONE`, `MN_VEHICLE_REGISTRATION`, `POSTAL_CODE`, `ALPHANUMERIC`. Custom regex support is guarded by length/time limits to avoid pathological patterns. ### Submissions A `Submission` groups one customer's answers (`contentTypeId`, `formId`, `conversationId`, `channelId`, `customerId`, `customFieldsData`, `submissions`); each `FormSubmission` row stores `formFieldId`, the submitted `text`/`value`, `formFieldText`/`formFieldType`, and `submittedAt`. Submissions attach to the customer record and can be logged onto related entities through `relation.onRelationAdded`. Submission export is registered as content type `frontline:formSubmission.formSubmission` (permission `formSubmissionsExportManage`). ### Form queries and mutations Schema: [`modules/form/graphql/schema/form.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/form/graphql/schema/form.ts). | Operation | Kind | Purpose | | --- | --- | --- | | `forms` / `formsMain` / `formsTotalCount` / `formDetail(_id)` | query | Form list/detail with `type`, `channelId`, `tagId`, `status`, `searchValue` filters | | `formSubmissions(filter…)` / `formSubmissionDetail(_id)` / `formsGetContentTypes` | query | Submission reads | | `cpForms` / `cpFormDetail` | query | Client portal reads | | `formsAdd` / `formsEdit` / `formsRemove` / `formsToggleStatus` / `formsDuplicate` | mutation | Form management | | `frontlineFields` / `frontlineFieldByCode` / `frontlineFieldsGetTypes` / `frontlineFieldsCombinedByContentType` | query | Field reads | | `frontlineFieldsAdd` / `frontlineFieldsEdit` / `frontlineFieldsRemove` / `frontlineFieldsBulkAction` / `frontlineFieldsUpdateOrder` / `frontlineFieldsUpdateVisible` / `frontlineFieldsUpdateSystemFields` | mutation | Field management | | `formSubmissionsSave` / `formSubmissionsRemove` / `formSubmissionsEdit` | mutation | Submission writes | | `widgetsLeadConnect` / `widgetsSaveLead` | mutation | Widget lead flow: connect a lead integration, then save a submission | | `cpWidgetsSaveLead` | mutation | Client portal lead submission | ```graphql mutation { formsAdd( name: "Contact us" title: "Contact us" type: "lead" channelId: "chan_1" ) { _id code } } ``` ### tRPC `form.submissionsByConversation` returns a conversation's form submissions. ## Surveys Survey operations are named `survey*` (older code called them polls: `poll*`, `withPoll`, `Ticket.sourcePoll`). ### Definitions A `Survey` carries `title`, `question`, `channelId`, `brandId`, `code`, `options`, ordered `steps` (each with its own `question` and `options`), `allowMultiselect`, `durationHours`, `status` (`active` or `archived`), `sentCount`, and `createdUserId`. Options can arm ticket automation (`ticketCreationEnabled`, `ticketCreationThreshold`, `ticketPipelineId`, `ticketStatusId`, `ticketCreated`, `ticketId`). ### Sending into a conversation `surveySendToConversation(_id, conversationId)` is a mutation that: - Requires an `active` survey. - Only targets Messenger conversations. - Checks channel and brand compatibility between the survey and conversation. - Writes a survey snapshot onto the message's `extraData.survey` and marks the conversation `hasSurvey: true`, which feeds the `withSurvey` inbox filter. > [!WARNING] > Survey conversations are excluded from `integrationType`-scoped inbox lists unless you pass `withSurvey: "true"`. ### Client portal submission `cpSurveySubmit(surveyCode, optionIds)` requires an authenticated client portal user. It finds or creates the customer, creates a Messenger conversation with a survey snapshot, validates option ids and `allowMultiselect` rules, and returns `alreadyVoted` on repeat votes. A partial unique index on `SurveyVotes` (`{ surveyId: 1, cpUserId: 1 }`, filtered to documents where `cpUserId` exists) enforces one vote per client-portal user per survey ([definitions](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/survey/db/definitions/surveys.ts)). After a successful vote, `runSurveyTicketAutomation` evaluates armed options; see [Tickets](https://erxes.io/docs/frontline-tickets#survey-driven-ticket-automation). ### Survey queries and mutations | Operation | Kind | Purpose | | --- | --- | --- | | `surveyList` / `surveyDetail(_id)` / `surveyTotalCount` | query | Admin list/detail with `searchValue`, `status`, `channelId` filters | | `cpSurveys` / `cpSurveyDetail(channelId, surveyCode)` / `cpSurveyVotes(conversationId)` | query | Client portal list, detail, and existing vote selections | | `surveyAdd` / `surveyEdit` / `surveyRemove` / `surveyToggleStatus` | mutation | Survey management | | `surveySendToConversation(_id, conversationId)` | mutation | Inject the survey into a Messenger conversation | | `cpSurveySubmit(surveyCode, optionIds)` | mutation | Client portal vote | There is no `cpSurveyVote` operation; the read is `cpSurveyVotes` and the write is `cpSurveySubmit`. --- # Knowledge Base Topics, categories, articles, and help center configuration Publish self-service content as topics → categories → articles, configure the client portal help center record that the published site reads, and run frontline report queries. Backend modules: `backend/plugins/frontline_api/src/modules/{knowledgebase,helpcenter,reports}`. Frontend: `frontend/plugins/frontline_ui/src/modules/{knowledgebase,helpcenter,report}` at `/frontline/knowledgebase`, `/frontline/helpcenter`, and `/frontline/reports`. ## Knowledge base ### Hierarchy Content is organized `Topic` → `Category` → `Article`: - A `Topic` is the top-level grouping published to portals (`title`, `description`, `brandId`, `color`, `icon`, `languageCode`). - A `Category` groups articles inside a topic (`title`, `description`, `icon`, `topicIds`, `parentCategoryId`, `numArticles`). - An `Article` carries `title`, `summary`, `content`, `status` (`draft`/`publish`), `isPrivate`, `reactionChoices`/`reactionCounts`, `topicId`, `categoryId`, `attachments`, `image`, `pdfAttachment`, publication fields (`createdUser`, `createdDate`, `modifiedDate`), and form codes that attach a lead form to the article. ### Knowledge base queries and mutations Schema: [`modules/knowledgebase/graphql/schemas/knowledgeBaseTypeDefs.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/knowledgebase/graphql/schemas/knowledgeBaseTypeDefs.ts). | Operation | Kind | Purpose | | --- | --- | --- | | `knowledgeBaseTopics` / `knowledgeBaseTopicDetail` / `knowledgeBaseTopicsTotalCount` | query | Topic reads | | `knowledgeBaseCategories` / `knowledgeBaseCategoryDetail` / `knowledgeBaseCategoriesTotalCount` / `knowledgeBaseCategoriesGetLast` | query | Category reads | | `knowledgeBaseArticles` / `knowledgeBaseArticleDetail` / `knowledgeBaseArticlesTotalCount` | query | Article reads | | `knowledgeBaseArticleDetailAndIncViewCount` / `knowledgeBaseArticlesIncrementViewCount` | query/mutation | Read while incrementing the article view counter | | `knowledgeBaseTopicsAdd` / `…Edit` / `…Remove` | mutation | Topic management | | `knowledgeBaseCategoriesAdd` / `…Edit` / `…Remove` | mutation | Category management | | `knowledgeBaseArticlesAdd` / `…Edit` / `…Remove` | mutation | Article management | | `cpKnowledgeBaseTopicDetail` | query | Client portal read used by the published help center | ### AI knowledge source The knowledge base registers an AI knowledge source through [`meta/automations`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/knowledgebase/meta/automations.ts): `ai.knowledgeSources` contains `{ key: 'knowledgebase.article', label: 'Knowledge base articles', moduleName: 'knowledgebase', sourceSelector: 'remote-module', supportsFullScope: true }`. Articles index with the category title included (articles are often titled numerically), so agent knowledge lookups match meaningful terms. ## Help Center ### The config record `HelpCenterConfig` is the single record a published help center site reads. `helpCenterConfigUpdate(config)` upserts it; sections include: - **General**: `title`, `description`, `url`, `erxesAppToken`, `brandId`, `languageCode`, knowledge-base toggle/label/`kbTopicId` (`kbToggle`, `kbLabel`), ticket toggle/label/`ticketChannelId`/`ticketPipelineId`/`ticketStatusId` (`ticketToggle`, `ticketLabel`). - **Appearance**: top-level `color` and `backgroundImage`, plus a `styles` object with `mainLogo`, `favicon`, `bodyColor`, `headerColor`, `footerColor`, `helpCenterColor`, `backgroundColor`, `activeTabColor`, `baseFont`/`baseColor`, `headingFont`/`headingColor`, `linkColor`/`linkHoverColor`, `primaryButtonColor`/`secondaryButtonColor`, `dividerColor`, and raw `headerHtml`/`footerHtml`. - **Header**: `wordmark`, `homeLabel`, `formsLabel`, `announcementsLabel`, `searchPlaceholder`. - **Footer**: `logo`, `description`, `copyright`, and `columns` (`heading` plus `links` of `label`/`url`). ### Domain resolution `helpCenterGetConfigByDomain` ignores its argument and reads the config from the request's `Origin` header. The header is normalized and matched (case-insensitively, prefix + `/`) against each config's `url`. Its `wrapperConfig.skipPermission: true` flag makes it callable without the `showHelpCenter` permission, which is what the published app calls before any user logs in. > [!WARNING] > If `helpCenterGetConfigByDomain` returns nothing, no config's `url` matches the request's `Origin`. Create one with `helpCenterConfigUpdate`. ### Help Center queries and mutations Schema: [`modules/helpcenter/graphql/schemas/helpCenterConfig.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/helpcenter/graphql/schemas/helpCenterConfig.ts). | Operation | Kind | Purpose | | --- | --- | --- | | `helpCenterConfig(_id)` / `helpCenterConfigs(page, perPage, searchValue, brandId)` / `helpCenterConfigsTotalCount` | query | Admin reads | | `helpCenterGetConfigByDomain` | query | Public resolution by request `Origin` (`skipPermission: true`) | | `helpCenterConfigUpdate(config)` / `helpCenterConfigRemove(_id)` | mutation | Upsert / remove | ### How the Help Center app uses it The standalone `apps/help-center` Next.js app (port `3900`, `NEXT_PUBLIC_ERXES_API_URL`, `NEXT_PUBLIC_APP_VERSION`) calls `helpCenterGetConfigByDomain` for configuration, then `cpKnowledgeBaseTopicDetail` for content, `cpForms`/`cpFormDetail` for forms, and `cpGetTickets`/`cpGetTicket`/`cpTicketGetNotes`/`cpCreateTicket`/`cpTicketCreateNote` for portal tickets. See [Help Center](https://erxes.io/docs/app-help-center). ## Reports The `reports` module backs `/frontline/reports`, `/frontline/calls/dashboard`, and `/frontline/calls/statistics`. | Area | Queries | Service | | --- | --- | --- | | Inbox | `reportConversationList`, `reportConversationOpen`/`Closed`/`Resolved`, `reportConversationTags`, `reportConversationSources`, `reportConversationResponses`, `reportConversationExport`, `conversationProgressChart` and member/source/tag progress | `reportConversation*` resolvers | | Tickets | `reportTicketList`, `reportTicketSource`, `reportTicketDate`, `reportTicketOpen`, `reportTicketTags`, `reportTicketCustomProperties`, `reportTicketStatusSummary`, `reportTicketPriority`, `reportTicketExport` | ticket report resolvers | | Calls | `callReportIntegrations`, `callGetQueueStats`, `callGetAgentStats`, `getCallbackStats`, `callVolumeSeries`, `callCarrierBreakdown`, `callHeatmap`, `callHeatmapDaily`, `callTopNumbers`, `callGetOperatorStats`, `callHistoryList` | `callReportService.ts`, `callHistoryService.ts`, `callStatistics.ts` | | Facebook | `reportFacebookPages`, `reportFacebookActivity`, `reportFacebookBots`, `reportFacebookSyncPostStats` | `facebookSyncService.ts` | | Saved charts | `reportCharts`, `reportChartDetail`, `reportChartAdd`, `reportChartEdit`/`Remove` (stored in `frontline_report_charts`) | chart resolvers | --- # Messenger Widget Embed the Frontline Messenger widget on your website and verify the conversation round trip Add the Frontline Messenger to your website so visitors can exchange messages with your team. If you run a different erxes release, copy the snippet generated by that installation. ## Prerequisites - A running erxes installation with the Frontline API and UI enabled. - Access to configure a Messenger integration and its channel/brand settings. - A deployed `frontline-widgets` application reachable from visitors' browsers. It is a separate Nx project under `apps/frontline-widgets` and is not deployed by starting Core UI and the gateway alone. See [Self-Hosting](https://erxes.io/docs/self-hosting). - Access to your website's HTML, layout, or tag manager. ## How the pieces fit The host page loads `messengerBundle.js`, a small IIFE built by esbuild from [`apps/frontline-widgets/src/index.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/frontline-widgets/src/index.ts). The loader injects a launcher and an iframe (`erxes-messenger-iframe`) whose `src` is the directory the bundle was loaded from, so `https://widgets.example.com/messengerBundle.js` implies the widget app at `https://widgets.example.com/`. The iframe app talks to the gateway's `/graphql` (HTTP and `graphql-ws` subscriptions) through `widgetsMessengerConnect`, `widgetsInsertMessage`, `widgetsConversations`, and the other `widgets*` operations. None require a user login. The install dialog builds the bundle URL from `REACT_APP_WIDGETS_URL` ([`frontend/plugins/frontline_ui/src/modules/utils.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/frontline_ui/src/modules/utils.ts)): `window.env.REACT_APP_WIDGETS_URL` first, then the build-time value, then a default derived from the current host. A `<subdomain>` placeholder is replaced with the leftmost host label. ## Install ### 1. Create the Messenger integration Open your Frontline channel/integration settings, create an erxes Messenger integration, and configure the channel, branding, and team settings. Save it, open its **Install script** action, and use **Preview** to check the appearance before embedding. The generated snippet sets `window.erxesSettings.messenger.integrationId` and loads `messengerBundle.js`. Older `brand_id` snippets or legacy Script Manager instructions describe a different widget version. ### 2. Add the generated snippet to your site Place it near the end of `body`, or in a shared layout that runs in the browser. Replace the placeholder values with the integration ID and widget URL from your installation: ```html <script> window.erxesSettings = { ...window.erxesSettings, messenger: { integrationId: "YOUR_MESSENGER_INTEGRATION_ID", }, }; (function () { const script = document.createElement("script"); script.src = "https://widgets.example.com/messengerBundle.js"; script.async = true; document.body.appendChild(script); })(); </script> ``` The spread preserves any other widget settings already on the page (the generated snippet assigns the object outright, so merge it yourself when combining widgets). Set `erxesSettings` **before** loading the bundle. The integration ID is browser-visible configuration; no app token is needed. ### 3. Verify the conversation round trip Open the site in a fresh browser session: 1. `messengerBundle.js` loads in the Network panel. 2. The widget iframe loads from the expected host and the launcher appears. 3. Send a test message, find it in the intended Frontline inbox/channel, and reply as a team member. 4. The visitor receives the reply. Also test a mobile viewport and a reload. A visible launcher proves the loader ran; the message round trip verifies the integration, gateway, and WebSocket connection together. ## Single-page applications Load the snippet in browser-only code after mount; never touch `window`/`document` during SSR. The loader detects an existing `erxes-messenger-container`, and if the bundle executes again it re-sends the current `erxesSettings`, `localStorage.erxes` data, and theme to the live iframe instead of mounting a second one. This is how a per-page `integrationId` change takes effect. Changing the settings object alone does not run the loader. ## Add a form alongside Messenger Frontline forms have their own **Install script** action (`install-form.tsx`). The form widget reads `window.erxesSettings.forms`, an array of `{ form_id, channel_id }`, loaded with `formBundle.js`. Configure both widgets before loading either bundle: ```html <div data-erxes-embed="YOUR_FORM_ID"></div> <script> window.erxesSettings = { ...window.erxesSettings, messenger: { integrationId: "YOUR_MESSENGER_INTEGRATION_ID" }, forms: [ { form_id: "YOUR_FORM_ID", channel_id: "YOUR_CHANNEL_ID" }, ], }; ["messengerBundle.js", "formBundle.js"].forEach(function (bundle) { const script = document.createElement("script"); script.src = "https://widgets.example.com/" + bundle; script.async = true; document.body.appendChild(script); }); </script> ``` The form dialog also offers an embedded target (`data-erxes-embed`) or a modal trigger (`data-erxes-modal="YOUR_FORM_ID"` on any button). Verify a submission lands in the configured channel. ## Knowledge content The Messenger integration itself carries a `knowledgeBaseTopicId`; when set, the widget shows an FAQ section from the Frontline knowledge base. Frontline UI also includes a topic **embed script** dialog that generates a `knowledgeBaseBundle.js` snippet. The `frontline-widgets` build only produces `messengerBundle.js` and `formBundle.js`, so that generated snippet has no bundle to load. For a full public help center, deploy `apps/help-center` instead (see [Help Center](https://erxes.io/docs/app-help-center)). ## Self-hosted widget configuration Two variables matter: - `REACT_APP_WIDGETS_URL`: where the install dialog points snippets. In the container, `docker-entrypoint.sh` writes all `REACT_APP_*` env vars to `/js/env.js` at start, so it can be set at run time; the nginx config substitutes `NGINX_HOST`/`NGINX_PORT`. - `REACT_APP_API_URL`: the gateway URL the iframe app calls. It is **inlined at build time** by `rspack.config.ts` (default `http://localhost:4000`); build the image with the correct value. ```bash pnpm nx show project frontline-widgets pnpm nx build frontline-widgets # depends on compile:index + compile:form (esbuild -> *Bundle.js) ``` Deploy the whole `dist/apps/frontline-widgets` output, not just the bundles. The iframe loads `index.html` and its assets from the same directory. > [!WARNING] > **The shipped nginx config blocks embedding** > > [`apps/frontline-widgets/nginx/default.conf`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/frontline-widgets/nginx/default.conf) adds `X-Frame-Options: SAMEORIGIN` on `location /`. As shipped, the messenger iframe can only render on the same origin as the widgets host, so it will not appear on your website. Remove or relax that header (and any equivalent `frame-ancestors` CSP rule) for the widget routes. ## Troubleshooting - `messengerBundle.js` returns 404: wrong `REACT_APP_WIDGETS_URL` or an incomplete deploy. The bundles are esbuild outputs copied as build assets; redeploy the full `dist` tree. - The launcher renders but the iframe is blank: the iframe `src` is the bundle's directory, so make sure `index.html` and assets exist there. Then check the browser console for a `REACT_APP_API_URL` baked at build time that points at localhost. - The iframe refuses to load: `X-Frame-Options: SAMEORIGIN`/`DENY` or CSP `frame-ancestors` on the widgets host (see the callout above), or CSP on your site blocking the frame/script. - "Integration not found" or the widget never connects: `integrationId` does not match a messenger-kind integration in this tenant (`widgetsGetMessengerIntegration`/`widgetsMessengerConnect` fail). Recopy the ID from the install dialog. - GraphQL requests blocked by CORS: the site origin is not in the gateway's `WIDGETS_DOMAIN`, `ALLOWED_DOMAINS`, or `ALLOWED_ORIGINS` list. - Messages send but replies never arrive: the `graphql-ws` subscription on `wss://<gateway>/graphql` is blocked by a proxy that does not upgrade WebSockets. - Settings vanish: a later snippet overwrote `window.erxesSettings`. Merge with the spread pattern above. - Duplicate launchers after client-side navigation: the loader is idempotent, so duplicates mean two different widget hosts or a tag manager firing the snippet plus a hard-coded one. - Generated `knowledgeBaseBundle.js` returns 404: `frontline-widgets` does not build it. Use the messenger FAQ or `apps/help-center`. For Google Tag Manager, put the generated snippet in a Custom HTML tag with triggers for the intended pages, and make sure no second installation loads the same widget. --- # Help Center Public knowledge base and ticket flows Run a public knowledge base and ticket portal that reads all of its content and configuration from erxes at request time. For the domain modules behind it, see [Frontline](https://erxes.io/docs/plugin-frontline). For the portal credential it uses, see [Client Portal](https://erxes.io/docs/app-client-portal). ## The app `apps/help-center` is a standalone Next.js 16.3 (React 19, Tailwind v4) portal (not an Nx project) with Apollo Client 4 for GraphQL. `npm run dev` serves it on port **3900**. It renders a support-portal landing page (ticket actions, announcements, knowledge base), article browsing and search, ticket submission/tracking, and portal-user sign-in. Every piece of content is read live from the gateway: | Area | GraphQL source | | --- | --- | | Portal identity, toggles, theme, header/footer | `helpCenterGetConfigByDomain` (Frontline helpcenter module, no login required) | | Knowledge base | `cpKnowledgeBaseTopicDetail` (Frontline knowledgebase) | | Announcements | `cpPostList` / `cpPost` (CMS, portal scope) | | Tickets | `cpCreateTicket`, `cpGetTickets`, `cpGetTicket`, `cpTicketGetNotes`, `cpTicketCreateNote` (Frontline tickets) | | Forms | `cpForms`, `cpFormDetail` (Frontline forms) | | Visitor session | `clientPortalUserRegister`, `clientPortalUserLoginWithCredentials`, `clientPortalLogout` | ## One variable, config-driven The only setting the app itself needs is `NEXT_PUBLIC_ERXES_API_URL`, the gateway base URL with no trailing slash. Everything else (portal name, app token, knowledge-base topic, ticket pipeline, labels, colors) comes from a **help center** record the backend looks up by the host each request arrives on. Create the record under **Frontline → Help Center** and set its **Website** to the address the portal is served from (`http://localhost:3900` for local work). The Website field is a Client Portal picker: choosing a portal also stores that portal's token on the config as `erxesAppToken`, which the app then sends as `x-app-token` for the `cp*` queries. Until a help center claims the request host, every page renders a "no help center configured" message naming the domain it looked for. The config's editable header fields (wordmark, section labels, search placeholder) and footer fields (logo, description, copyright, link columns) are exposed in the `helpCenterGetConfigByDomain` payload and edited from the Help Center drawer in Frontline UI. See [Knowledge Base, Help Center & Reports](https://erxes.io/docs/frontline-knowledge-base). | Variable | Purpose | | --- | --- | | `NEXT_PUBLIC_ERXES_API_URL` | Gateway URL, e.g. `https://officenext.erxes.io/gateway` | | `NEXT_PUBLIC_APP_VERSION` | `OS` (default) or `SAAS`. On `SAAS`, a `<subdomain>` placeholder in the API URL is filled from the request host; one container serves a help center per tenant | ```dotenv NEXT_PUBLIC_ERXES_API_URL=https://officenext.erxes.io/gateway NEXT_PUBLIC_APP_VERSION=OS ``` ## Runtime configuration and Docker `NEXT_PUBLIC_*` values are inlined into the client bundle at `next build`, which would tie an image to one gateway. To avoid that, [`docker-entrypoint.sh`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/help-center/docker-entrypoint.sh) writes `public/js/env.js` from the container environment before the server starts, the root layout loads that file first, and [`modules/apollo/utils/env.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/help-center/modules/apollo/utils/env.ts) reads `window.env` at run time. `next dev`/`next start` read `.env.local` the usual way. Build from the **repository root** (the app compiles form controls from the `erxes-ui` source tree): ```bash docker build -f apps/help-center/Dockerfile -t erxes/help-center . docker run -p 3900:3900 \ -e NEXT_PUBLIC_ERXES_API_URL=https://officenext.erxes.io/gateway \ erxes/help-center ``` The image uses Next's `standalone` output; `PORT` (default `3900`) and `HOSTNAME` (default `0.0.0.0`) are read at start. Behind a proxy, forward the original host; that is what the help center lookup matches on: ```nginx proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; ``` On a SaaS install, combine `NEXT_PUBLIC_APP_VERSION=SAAS` with a placeholder gateway like `https://<subdomain>.api.erxes.io/gateway`; a request to `acme.help.example.com` then talks to `acme`'s gateway. ## Routes | Route | Backing data | | --- | --- | | `/` | Portal landing: ticket actions, announcements, KB sections | | `/knowledge-base`, `/knowledge-base/category/[id]`, `/knowledge-base/article/[id]` | `cpKnowledgeBaseTopicDetail` | | `/search?q=` | KB articles + CMS announcements, labelled by type | | `/tickets`, `/tickets/new`, `/tickets/track`, `/tickets/[id]` | `cpGetTickets`, `cpCreateTicket`, `cpGetTicket`, `cpTicketGetNotes` | | `/announcements`, `/announcements/[slug]` | `cpPostList`, `cpPost` | | `/account`, `/sign-in`, `/sign-up` | `clientPortalUser*` mutations | The whole app is rendered per request. `app/layout.tsx` exports `dynamic = 'force-dynamic'`, so config, KB content, and announcements are fetched fresh on every hit rather than statically cached. When the app runs against an older gateway, `modules/config/api.ts` retries the config query without the header/footer block and then with a `domain` argument, so a newer portal image still works against older gateways ([query](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/help-center/modules/config/graphql/queries/helpCenterConfig.ts)). ## Check it works 1. Point the app at your gateway, open `/`, and confirm the portal name and sections come from your help center record, not from env. 2. Change the help center's Website to a different domain and confirm requests on that host find the other config. 3. Submit a ticket (`/tickets/new`) and confirm it lands in the configured pipeline; track it from `/tickets/track` and as a signed-in portal user from `/tickets`. 4. In the container, unset `NEXT_PUBLIC_ERXES_API_URL` and confirm pages fail fast rather than silently hitting a stale build-time URL. ## Troubleshooting - **"No help center" page**: the request host does not match any help center's Website field, or the proxy dropped `X-Forwarded-Host`. - **KB renders but tickets/announcements 401**: the config's `erxesAppToken` is stale; re-pick the portal in the Website field to re-store a current token. - **Wrong tenant's content on SaaS**: `NEXT_PUBLIC_APP_VERSION` is not `SAAS`, or the API URL lacks the `<subdomain>` placeholder. - **Config errors after upgrading the image only**: the gateway predates the header/footer fields; the app retries automatically, so upgrade the backend rather than pinning the image. --- # Sales Deals, pipelines, POS, and ecommerce Deal pipelines, point-of-sale configuration and orders, and storefront ecommerce helpers. Sales provides the deal pipeline (boards → pipelines → stages → deals with labels, checklists, watchers, and per-deal product/payment data), the POS configuration and order API used by the separate `posclient` plugin, and a small ecommerce module (wishlists, reviews, addresses, last-viewed items) exposed over HTTP routes and GraphQL. It registers with the platform through `meta/`: permissions, automations, notifications, the `sales:sales.deals` and `sales:pos.orders` segment definitions, `sales:deal` document generation, record references (including `excludeLoyaltyAmount`), and the `deal` property type. `sales_api` contains the POS *configuration and order APIs*. `posclient_api` and the `posclient-front` app are the separately deployed storefront; they sync through this plugin's `/pos-init`, `/pos-sync-config`, and `/get-pos-token` routes ([`src/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/routes.ts)) and the `pos.createOrUpdateOrders*` tRPC procedures ([`src/trpc/init-trpc.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/trpc/init-trpc.ts)). For the product catalog itself, see [Products](https://erxes.io/docs/core-products); for the deployed POS storefront, see [POS Client](https://erxes.io/docs/app-pos-client). ## Enabling and ports ```dotenv ENABLED_PLUGINS=sales ``` The entry maps to `sales_api` and `sales_ui`; dev scripts append `_api`/`_ui` to each name in `ENABLED_PLUGINS`. | Project | Role | Dev port | | --- | --- | --- | | `sales_api` | Federated GraphQL subgraph + tRPC + POS/ecommerce HTTP routes; subscriptions enabled | `3305` | | `sales_ui` | Module Federation remote | `3005` | The API port is set in [`main.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/main.ts). The POS storefront backend (`posclient_api`, port `3312`) and app (`apps/posclient-front`, dev port `7002`) are separate; see [Point of Sale](https://erxes.io/docs/sales-pos) and [POS Client](https://erxes.io/docs/app-pos-client). ## Module map | Module | What it contains | Key collections | Guide | | --- | --- | --- | --- | | `modules/sales/` | Boards, pipelines, stages, deals, labels, checklists; segment fields, documents, references | `Deals`, `Pipelines`, `Stages`, `Boards` | [Deals & Pipelines](https://erxes.io/docs/sales-deals-pipelines) | | `modules/pos/` | POS configs, orders, covers, slots, product groups; POS sync routes | `Pos`, `PosOrders`, `PosCovers`, `PosSlots` | [Point of Sale](https://erxes.io/docs/sales-pos) | | `modules/ecommerce/` | Wishlists, product reviews, addresses, last-viewed items | `Wishlist`, `ProductReview`, `Address`, `LastViewedItem` | [Ecommerce](https://erxes.io/docs/sales-ecommerce) | | `meta/` | permissions, automations, notifications, segments, documents, references, properties, beforeResolvers, afterProcess | platform extension points | this page and module pages | Frontend `sales_ui` modules mirror this: `deals/` (boards, pipelines, deal detail, product/payment data), `payments/`, `pos/` (POS settings under `settings/sales/*`). `src/config.tsx` registers `sales/deals`, `sales/pos`, and `settings/sales/*` navigation. ## How it connects to Core - **Properties**: `meta/properties.ts` declares the `deal` property type with its `systemFields`; pipeline `propertyIds` are validated against Core `sales:deal` fields over tRPC (`fields.find`). - **Segments**: `meta/segments.ts` merges the `sales` and `pos` module definitions: `sales:sales.deals` and `sales:pos.orders` content types, `customer.deals`/`company.deals` relations backed by Core relation records, plus `evaluateFields`, `listSegmentMembers`, `countSegmentMembers`, and `applyMembership` producers. See [Properties, Tags & Segments](https://erxes.io/docs/core-properties-tags-segments). - **Automations**: `meta/automations.ts` registers deal triggers/actions and the POS-order event trigger; see [Automations & Approvals](https://erxes.io/docs/core-automations-approvals). - **Documents**: `meta/documents.ts` registers `sales:deal` for document templates; `documents.editorAttributes` and `deal.replaceContent` tRPC procedures provide the editor merge fields. See [Documents](https://erxes.io/docs/core-forms-documents). - **Payments**: `meta.payments` callbacks apply paid transactions to `mobileAmount`/`mobileAmounts` on deals and forward POS-order payments to `posclient` over tRPC. Payment providers are part of the Enterprise Edition. - **Import/export**: one export type, `sales:pos.posItems`, gated by `posItemsExportManage`. ## Configuration Env vars the plugin reads directly: | Variable | Purpose | | --- | --- | | `GET_CP_TOKEN` | Shared secret for `GET /get-pos-token`, which lists POS names and tokens for the client-portal flow | | `ALLOW_OFFLINE_POS` | When truthy, lets `pos.create` keep `onServer` unset (offline-capable POS); otherwise new POS docs are forced `onServer` | | `DOMAIN` | Base URL used to build deal/board links (`{DOMAIN}/deal/board?...`) | | `TIMEZONE`, `NODE_ENV` | Timezone for date handling; production flag for the subscription bundle | POS credentials live on the POS document itself (`token`, `adminIds`, `cashierIds`, `paymentIds`). ## Pages in this section - [Deals & Pipelines](https://erxes.io/docs/sales-deals-pipelines): Boards, pipelines, stages, deals, labels, and checklists - [Point of Sale](https://erxes.io/docs/sales-pos): POS configuration, orders, sync routes, and the posclient API - [Ecommerce](https://erxes.io/docs/sales-ecommerce): Storefront helpers, carts, and order endpoints - [POS Client](https://erxes.io/docs/app-pos-client): Offline-capable storefront and posclient API --- # Deals & Pipelines Boards, pipelines, stages, deals, labels, and checklists Deal boards, pipelines, stages, deals, labels, and checklists: the core of the Sales plugin. ## Module layout Backend code lives under `backend/plugins/sales_api/src/modules/sales/`: | Area | Path | Responsibility | | --- | --- | --- | | Models | `db/definitions`, `db/models` | Mongoose schemas for boards, pipelines, stages, deals, labels, checklists | | GraphQL | `graphql/schemas`, `graphql/resolvers` | Queries, mutations, custom resolvers, loaders | | tRPC | `trpc/deal.ts`, `trpc/document.ts` | Cross-service procedures | | Documents | `documents/dealContent.ts`, `documents/replaceBlocks.ts` | Deal document-template content replacement | | Meta | `meta/automations`, `meta/segments`, `meta/references`, `meta/activity-log`, `meta/payments` | Automation constants/handlers, segment definitions, record references | The frontend counterpart is `frontend/plugins/sales_ui/src/modules/deals/` (boards, pipelines, deal detail, product/payment UI). ## Data model The hierarchy is `Board → Pipeline → Stage → Deal`, each collection scoped per tenant: - **Board** (`Boards`): `name`, `order`, `type` (default `deal`). - **Pipeline** (`Pipelines`): `name`, `boardId`, `visibility`, `memberIds`, `watchedUserIds`, `departmentIds`, `branchIds`, `paymentTypes` (may carry `scoreCampaignId`), `propertyIds`, plus card-numbering and date-check settings. - **Stage** (`Stages`): `name`, `pipelineId`, `probability`, `status`, `visibility`, `memberIds`/`canMoveMemberIds`/`canEditMemberIds`, `formId`, `order`. - **Deal** (`Deals`): `name`, `stageId`, `order`, `assignedUserIds`, `watchedUserIds`, `labelIds`, `tagIds`, `branchIds`/`departmentIds`, `priority`, `status` (`active`/`archived`, `SALES_STATUSES`), `startDate`/`closeDate`/`stageChangedDate`, `productsData` (per-product quantity, prices, `tickUsed`, `discountInfos`), `paymentsData`, `totalAmount`/`unUsedTotalAmount`/`bothTotalAmount`, `mobileAmount`/`mobileAmounts`, `propertiesData`, `customFieldsData`, `number`, `relations` (Gantt links), `timeTrack`. - **Labels** and **checklists**: pipeline-scoped `SalesPipelineLabel` records and deal-attached `SalesChecklist`/`SalesChecklistItem` records. A deal stores no `customerId`/`companyId`. Customer and company links are Core relation records read by the platform, not fields on the deal. ### Pipeline property selection `Pipelines.propertyIds` stores ids of Core `sales:deal` fields. `salesPipelinesAdd`/`salesPipelinesEdit` validate every id through Core's `fields.find` tRPC procedure before writing ([`modules/sales/utils/pipelineProperties.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/sales/utils/pipelineProperties.ts)). Deal detail renders only the selected properties; a separate `isPropertySelectionConfigured` flag preserves legacy show-all behavior for pipelines saved before the feature existed. ## Queries and mutations Schema: [`modules/sales/graphql/schemas/deal.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/sales/graphql/schemas/deal.ts). | Operation | Kind | Purpose | | --- | --- | --- | | `deals(stageId, filter…)` / `dealDetail(_id)` | query | Cursor-paginated deal list and detail | | `dealsTotalCount` / `dealsTotalAmounts` / `archivedDeals` | query | Counts, per-currency totals, archive browsing | | `dealsAdd` / `dealsEdit` / `dealsRemove` / `dealsCopy` | mutation | Deal CRUD | | `dealsChange(itemId, destinationStageId, aboveItemId)` | mutation | Move a card between stages or reorder | | `dealsWatch(_id, isAdd)` / `dealsArchive(stageId)` | mutation | Watch a deal; archive a whole stage | | `dealsCreateProductsData` / `dealsEditProductData` / `dealsDeleteProductData` | mutation | Product rows on a deal | | `checkDiscount(_id, products, couponCode, voucherId)` | query | Coupon/voucher price check for deal products; provided by the Enterprise Edition | | `salesBoards` / `salesBoardDetail` / `salesBoardsAdd` / `salesBoardsEdit` / `salesBoardsRemove` | query/mutation | Boards | | `salesPipelines` / `salesPipelineDetail` / `salesPipelinesAdd` / `salesPipelinesEdit` / `salesPipelinesUpdateOrder` / `salesPipelinesWatch` / `salesPipelinesRemove` / `salesPipelinesArchive` / `salesPipelinesCopied` | query/mutation | Pipelines, including `propertyIds` | | `salesStages` / `salesStageDetail` / `salesStagesEdit` / `salesStagesUpdateOrder` / `salesStagesSortItems` / `salesArchivedStages` | query/mutation | Stages and card ordering | | `salesPipelineLabels` / `salesPipelineLabelsAdd` / `salesPipelineLabelsLabel` | query/mutation | Labels and applying them to deals | | `salesChecklists` / `salesChecklistsAdd` / `salesChecklistItemsAdd` / `salesChecklistItemsOrder` | query/mutation | Checklists and items | Most list/read operations also have `cp*` variants (`cpDeals`, `cpDealDetail`, `cpSalesStages`, …) marked `forClientPortal` for storefront use; see [Ecommerce](https://erxes.io/docs/sales-ecommerce) and [Client Portal](https://erxes.io/docs/app-client-portal). Subscriptions: `salesDealChanged(_id)` and `salesDealListChanged(pipelineId, userId, filter)` push live board updates. Example: add a deal (signature from `graphql/schemas/deal.ts`): ```graphql mutation { dealsAdd( name: "Q3 hardware renewal" customerIds: ["cust_1"] stageId: "stage_open" assignedUserIds: ["user_1"] closeDate: "2026-12-31" productsData: [{ productId: "prod_7", quantity: 4, unitPrice: 250, tickUsed: true }] ) { _id name } } ``` ## tRPC [`src/trpc/init-trpc.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/trpc/init-trpc.ts) merges the `deal`, `pos` (with `orders`), `documents`, and `fields` routers. Deal-side procedures include `deal.findOne`, `deal.find`, `deal.count`, `deal.aggregate`, `deal.tag`, `deal.createItem`/`editItem`/`removeItem`, `deal.generateAmounts`, `deal.generateProducts`, `deal.replaceContent`, `deal.contentIds`, `deal.subscriptionWrapper`, `deal.create`/`updateOne` (system-user), `stage.findOne`/`stage.find`, `pipeline.findOne`, and `documents.editorAttributes` / `fields.getFieldList`. Always pass a `limit` to `deal.find`. It forwards `skip`/`limit`/`sort` to MongoDB, and an unbounded `deal.find` over a large tenant can exhaust the service. ## Automations, segments, documents - **Automations** (`meta/automations.ts` + `modules/sales/meta/automations/`): triggers are "Sales pipeline" (segment enrollment), "Deal reaches stage probability", and "Deal stage changed"; actions are "Create deal" and "Create sales checklist". `setPropertyTargets` let workflows write customer/company deal relations. - **Segments**: the `sales:sales.deals` content type with filterable fields, member listing/counting, and membership writes; `customer.deals` and `company.deals` relations join through Core relation records. Stage-derived fields (`pipelineId`, `boardId`, `stageProbability`) rewrite their conditions to `stageId`s before evaluating. - **Documents**: `sales:deal` is a document content type. `documents.editorAttributes` returns merge fields (fixed attributes, schema fields, and Core custom fields as `customFieldsData.<fieldId>`); `deal.replaceContent` renders one processed document per selected deal in the caller's order. - **References** (`meta/references.ts`): deal display names, links, labels, product amount helpers, and `excludeLoyaltyAmount` (deal total minus payments through types carrying `scoreCampaignId`; used by the Enterprise Edition). ## Permissions `meta/permissions.ts` registers modules `deal` (`showDeals`, `dealsAdd`, `dealsEdit`, `dealsProductsEdit`, `dealsRemove`, `dealsWatch`, `dealsArchive`), `board` (`boardsAdd/Edit/Remove`, `updateTimeTracking`), `pipeline` (`pipelinesAdd/Edit/Watch`), `stage` (`stagesEdit`, `stagesRemove`, `itemsSort`), `checklist`, `pipelineLabel`, and `pipelineTemplate`, with `own`/`group`/`all` scopes on deals. Default groups: `sales:admin`, `sales:user`, `sales:viewer`. ## Troubleshooting - **Pipeline edit rejects `propertyIds`**: every id must be a Core `sales:deal` field; validation calls Core `fields.find` and fails closed. - **Deal list is slow or unsorted**: the unscoped list relies on the `parentId/order/_id/status` compound index; filters outside it can force a collection scan. - **A segment never updates for deal writes**: the content type must be declared as `sales:sales.deals`; the event dispatcher matches on that string. --- # Point of Sale POS configuration, orders, sync routes, and the posclient API POS configuration, orders, covers, and slots inside the Sales plugin, plus the sync API the standalone POS client uses. Two services are involved: `sales_api` (this module, port 3305) stores POS records and orders, and `posclient_api` (port 3312) serves the offline-capable storefront app. For the storefront itself, see [POS Client](https://erxes.io/docs/app-pos-client). ## Module layout Backend code lives under `backend/plugins/sales_api/src/modules/pos/`: | Area | Path | Responsibility | | --- | --- | --- | | Models | `db/definitions/{pos,orders,covers}.ts`, `db/models` | `Pos`, `PosOrders`, `PosCovers`, `PosSlots`, `ProductGroups` | | GraphQL | `graphql/schemas`, `graphql/resolvers` | POS config, order, cover, product-group operations | | HTTP routes | `routes.ts` (mounted from `src/routes.ts`) | `/pos-init`, `/pos-sync-config`, `/get-pos-token` | | tRPC | `trpc/pos.ts` | `pos.*` and `orders.*` routers | | Meta | `meta/automations`, `meta/segments`, `meta/export` | POS-order event trigger, `sales:pos.orders` segment type, POS-items export | POS settings screens live in `frontend/plugins/sales_ui/src/modules/pos/` under `settings/sales/*`. ## Data model - **`Pos`**: one document per terminal/store: `name`, `token`, `adminIds`/`cashierIds`, `isOnline`/`onServer`, `branchId`/`departmentId`, `allowBranchIds`, `paymentIds`, `paymentTypes`, `productDetails`, `catProdMappings`, `initialCategoryIds`, `deliveryConfig`, `kioskMachine`/`kitchenScreen`/`waitingScreen`, `permissionConfig` (e.g. `cashiers.seeReport`), `isCheckRemainder`/`saveRemainder`, `beginNumber`/`skipNumber`. - **`PosOrders`**: synced orders: `number`, `status`, `paidDate`, `customerId`/`customerType`, `posToken`, `items`, `paidAmounts`, `cashAmount`/`mobileAmount`, `totalAmount`, `deliveryInfo` (may carry `dealId`), `subscriptionInfo`, `registerNumber`, `convertDealId`. - **`PosCovers`**: shift/cash covers with a `note`. - **`PosSlots`** and **`ProductGroups`**: table slots and the per-POS category groupings (with `categoryIds`, `excludedCategoryIds`, `excludedProductIds`) that shape the storefront catalog. ## HTTP routes Mounted on the plugin's Express app ([`modules/pos/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/pos/routes.ts)), reached through the gateway as `/pl:sales/<path>`: | Route | Handler | Purpose | | --- | --- | --- | | `GET /pos-init` | `posInit` | Full bootstrap for a POS token (header `pos-token`): config + users + product groups + slots | | `POST /pos-sync-config` | `posSyncConfig` | Partial resync by `type`: `config`, `products`, `slots`, `productsConfigs` | | `GET /get-pos-token` | `getPosToken` | Lists `{ name, token }` for every POS; requires `?GET_CP_TOKEN=` matching the `GET_CP_TOKEN` env var | `posInit`/`posSyncConfig` assemble data across services: admins and cashiers come from Core `users.find`, and product categories/products from Core `productCategories.find`/`products.find`, all over tRPC. They also pull tax-receipt settings and pricing discounts from Enterprise Edition plugins. > [!WARNING] > **Protect GET_CP_TOKEN** > > `GET /get-pos-token` returns POS tokens for the whole tenant. Keep `GET_CP_TOKEN` secret; it is meant for the client-portal flow, not general integrations. ## Queries and mutations Schemas live under [`modules/pos/graphql/schemas`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/pos/graphql/schemas). | Operation | Kind | Purpose | | --- | --- | --- | | `posList` / `posDetail(_id)` / `posEnv` | query | POS configs; `posEnv` exposes `ALLOW_OFFLINE_POS` | | `posAdd` / `posEdit` / `posRemove` | mutation | POS lifecycle | | `productGroups` / `productGroupsAdd` / `productGroupsBulkInsert` | query/mutation | Catalog grouping per POS | | `posSlots` / `posSlotBulkUpdate` | query/mutation | Table slots | | `posProducts(…)` | query | Catalog fetch against a POS scope | | `posOrders` / `posOrdersList` / `posOrderDetail` / `posOrderLink` | query | Order lists and detail | | `posOrdersSummary` / `posOrdersGroupSummary` / `posOrdersTotalCount` / `posOrderRecords` / `posOrderRecordsCount` | query | Aggregates and item records | | `posOrderCustomers(+TotalCount)` / `posOrderBySubscriptions(+TotalCount)` / `checkSubscription` | query | Customer and subscription views | | `posOrderChangePayments(_id, …)` | mutation | Adjust payments on an order | | `posCovers` / `posCoversCount` / `posCoverDetail` / `posCoversEdit` / `posCoversRemove` | query/mutation | Covers | | `ecommerceGetBranches(posToken)` | query | Branch list for a POS token | ## Sync with posclient_api The storefront backend pulls config and pushes orders: - **Config pull**: `posConfigsFetch(token)` on `posclient` calls `GET /pl:sales/pos-init` and stores the result in its `Configs` collection; `syncConfig(type)` re-pulls `config`/`products`/`slots`/`productsConfigs` through `POST /pl:sales/pos-sync-config`. - **Order push**: `syncOrders` on `posclient` batches up to 100 unsynced paid orders (with items and receipt responses) and calls `pos.createOrUpdateOrdersMany` over tRPC; `pos.createOrUpdateOrders` handles single-order `statusToDone` and payment sync. - **Hourly remainder sync**: `posclient_api`'s BullMQ scheduler `posclient-sync-remainder` (`0 * * * *` UTC) runs `syncRemainders`/`syncDiscounts` per tenant for every config with `saveRemainder`. - **Payment callbacks**: `meta.payments.callback` in `sales_api` forwards paid `sales:pos.orders` transactions to `posclient.paymentCallbackClient` over tRPC. ## tRPC [`trpc/pos.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/pos/trpc/pos.ts) exposes `pos` and `orders` sub-routers: `pos.findOne`/`find`/`create`/`confirmCover`/`ecommerceGetBranches`/`ordersDeliveryInfo`/`createOrUpdateOrders`/`createOrUpdateOrdersMany`, and `orders.findOne`/`find`/`updateOne`. ## Automations and segments `modules/pos/meta/automations` registers a custom trigger "POS order event" (`sales:pos.orders`) with event types `created`, `paid`, `returned`, `statusChanged`, `paymentChanged`, `deliveryCompleted`, and an action that creates POS orders. The `sales:pos.orders` segment content type is declared under `meta/segments/` with field filters, member listing, and a `customer.orders` relation; it needs an index on `pos_orders.customerId` for relation measures. ## Permissions The `pos` permission module registers `posRead`, `posOrderRead`, `posCoversRead` (all `always`), plus `posAdd`/`posEdit`/`posRemove`, `posOrderChangePayments`, `posCoversEdit`/`posCoversRemove`, `posSlotBulkUpdate`, `productGroupsBulkInsert`, and `posItemsExportManage` (the `sales:pos.posItems` export type). ## Troubleshooting - **`pos-init` returns "Not found POS by token"**: the `pos-token` header is missing or doesn't match a `Pos` document's `token`. - **`pos-sync-config` returns "wrong type"**: `type` must be one of `config`, `products`, `slots`, `productsConfigs`. - **Remainders never sync**: `saveRemainder` must be set on the POS config, and the `posclient` worker must be running. - **New POS is forced online**: without `ALLOW_OFFLINE_POS`, `pos.create` sets `onServer`, which triggers `syncPosToClient` on save. --- # Ecommerce Storefront helpers, carts, and order endpoints Storefront-facing customer data (wishlists, product reviews, last-viewed items, and addresses) inside the Sales plugin. The `ecommerce` module lives inside `sales_api` (port 3305). It exposes authenticated GraphQL operations for staff, `cp*` operations marked `forClientPortal` for storefront customers, and unauthenticated HTTP summary routes for storefront bootstrapping. ## Module layout Backend code lives under `backend/plugins/sales_api/src/modules/ecommerce/`: | Area | Path | Responsibility | | --- | --- | --- | | Models | `db/definitions`, `db/models` | `Wishlist`, `ProductReview`, `LastViewedItem`, `Address` | | GraphQL | `graphql/schema`, `graphql/resolvers` | Admin and `cp*` (Client Portal) operations | | HTTP routes | `routes.ts` (mounted from `src/routes.ts`) | `/ecommerce-*` summary and bulk endpoints | | tRPC | `trpc/ecommerce.ts` | Router file exists but is **not merged** into the plugin `appRouter` | | Utils | `utils.ts` | Core `products.find`, `customers.find`, `productCategories.find` fetchers | ## Data model - **`Wishlist`**: `{ productId, customerId }` pairs. - **`ProductReview`**: `{ productId, customerId, review: Float, description, info: JSON }`. - **`LastViewedItem`**: `{ productId, customerId, modifiedAt }` with a `{ customerId, productId }` index. - **`Address`**: `{ alias, customerId, coordinate: {lat,lng}, address1, address2, city, district, street, detail, more: JSON, w3w, note, phone }` plus timestamps. `productId`/`customerId` reference Core records; the module never stores product or customer data. It hydrates them over tRPC (`core.products.find`, `core.customers.find`, `core.productCategories.find`). ## HTTP routes Mounted on the plugin Express router ([`modules/ecommerce/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/ecommerce/routes.ts)), reached through the gateway as `/pl:sales/<path>`. All are `GET` unless noted; all take `customerId`/`productId` as query params and return `{ status, data }` or `{ status: 'error', errorMessage }`: | Route | Params | Returns | | --- | --- | --- | | `GET /ecommerce-init` | `customerId` | Customer summary: reviews + stats, wishlist, last 20 viewed items, addresses, hydrated products | | `GET /ecommerce-product-summary` | `productId` | Reviews, rating stats, wishlist/last-viewed counts, reviewer customers | | `GET /ecommerce-last-viewed` | `customerId`, `limit` (default 10) | Last-viewed items with hydrated products | | `GET /ecommerce-wishlist` | `customerId` | Wishlist with hydrated products | | `GET /ecommerce-addresses` | `customerId` | Address list | | `GET /ecommerce-product-reviews` | `productId`, `limit` (20), `skip`, `sort` (`-createdAt`) | Reviews with hydrated customers plus summary | | `POST /ecommerce-bulk-operations` | `operations[]` | Batch write: `addToWishlist`, `addReview`, `addLastViewed`, `addAddress` | > [!WARNING] > **These routes are unauthenticated** > > `/ecommerce-*` routes check no token; any caller who knows a `customerId` can read that customer's wishlist, addresses, and review history. Front them with your own gateway auth if the storefront is public-facing. ## Queries and mutations ### Wishlist | Operation | Kind | Notes | | --- | --- | --- | | `wish(productId, customerId)` | query | Single wish, hydrated `product` | | `wishlist(customerId)` | query | List, hydrated products | | `wishlistAdd` / `wishlistUpdate` / `wishlistRemove` | mutation | Staff-side mutations | | `cpWishlist`, `cpWishlistAdd/Update/Remove` | query/mutation | `forClientPortal` variants | ### Product reviews | Operation | Kind | Notes | | --- | --- | --- | | `productReview(productId)` | query | `AverageReview`: average + count | | `productReviews(productIds, customerId, page, perPage)` | query | Paginated list | | `productReviewAdd/Update/Remove` | mutation | All review mutations are `skipPermission` | | `cpProductReviews`, `cpProductReview` | query | `forClientPortal` | | `cpProductReviewAdd/Update/Remove` | mutation | `skipPermission` | ### Last viewed items | Operation | Kind | Notes | | --- | --- | --- | | `lastViewedItems(customerId, limit)` | query | Staff-side | | `lastViewedItemAdd` / `lastViewedItemRemove` | mutation | Staff-side | | `cpLastViewedItems`, `cpLastViewedItemAdd/Remove` | query/mutation | `forClientPortal` | ### Addresses | Operation | Kind | Notes | | --- | --- | --- | | `address(_id)` / `addressList(page, perPage, searchValue, aliasType, customerId, city, district, street)` | query | `addressList` is `forClientPortal` | | `addressAdd` / `addressUpdate` / `addressRemove` | mutation | `addressAdd`/`addressUpdate` are `forClientPortal` | ## Permissions and context The module registers no permission actions of its own. Standard operations run under the platform's default resolver wrapper, which only requires a logged-in user (`checkLogin`). There are no per-action ecommerce permissions. Operations marked `forClientPortal: true` run under `wrapPublicResolver` instead ([`wrapperResolvers.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/wrapperResolvers.ts)): they require a Client Portal request context (`context.clientPortal`) and skip staff login. No ecommerce operation sets `cpUserRequired`, so a CP user session is not enforced; pass `customerId` explicitly. See [Client Portal app](https://erxes.io/docs/app-client-portal) for how that context is established. ## tRPC [`trpc/ecommerce.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/ecommerce/trpc/ecommerce.ts) defines `productReview`, `wishlist`, `lastViewedItem`, and `address` sub-routers with `findOne`/`find`/`aggregate`/`count`/`create`/`update`/`remove` helpers, but the file is **not imported by `src/trpc/init-trpc.ts`**, so none of these procedures are reachable through the plugin's `/trpc` mount. Treat the router as dead code; use the HTTP routes or GraphQL instead. ## Troubleshooting - **`Customer ID is required` / `Product ID is required`**: the routes read query params, not a body; use `?customerId=`. - **Hydrated `product` is `null`**: the referenced Core product was deleted or belongs to another tenant; list resolvers drop wishes whose product no longer exists. - **tRPC call to `productReview.find` fails**: the ecommerce router isn't mounted; nothing to fix in config, use HTTP/GraphQL. --- # POS Client Offline-capable storefront and posclient API Run the offline-tolerant point-of-sale storefront against the `posclient` backend plugin. For the POS configuration in the admin UI, see [Sales](https://erxes.io/docs/plugin-sales) and [Point of Sale](https://erxes.io/docs/sales-pos). For gateway headers, see [Authentication & Permissions](https://erxes.io/docs/api-authentication). ## Components | Piece | Path | Notes | | --- | --- | --- | | Backend plugin | `backend/plugins/posclient_api` | `startPlugin` name `posclient`, port **3312**, GraphQL + tRPC + subscriptions | | Storefront app | `apps/posclient-front` | Standalone Next.js 14 (React 18) PWA via `next-pwa`, `next dev -p 7002` | There is no `frontend/plugins/posclient_ui`; the storefront is the standalone app and POS settings live in the Sales UI. ## Backend shape Beyond the standard plugin GraphQL, [`src/main.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/posclient_api/src/main.ts) sets up: - **Express routes** ([`src/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/posclient_api/src/modules/posclient/routes.ts)): `GET /initial-setup` and `POST /pl:posclient/initial-setup` both run `posInitialSetup`. The handler reads the tenant from the request host, loads the POS config, and writes each entry of the `?envs={...}` JSON query map as a cookie. This is how a kiosk browser receives its `NEXT_PUBLIC_*` values when the POS image cannot be rebuilt per site. - **Config middleware** (`src/configMiddleware.ts`): reads the `erxes-pos-token` header or the `pos-config-token` cookie and loads the matching `Configs` document into `req.posConfig`. - **User middleware** (`src/userMiddleware.ts`): reads the `pos-auth-token` cookie, verifies it against `JWT_TOKEN_SECRET`, and sets `req.posUser`. - **Context** (`apolloServerContext`): `context.config` is `req.posConfig`; if no token was sent, it falls back to the single non-deleted config when exactly one exists. `context.posUser` gates user-level operations. - **CORS**: `credentials: true` with origins compiled from `ALLOWED_ORIGINS` regexes. ### Scheduled sync workers [`src/worker/`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/posclient_api/src/worker/index.ts) registers a BullMQ scheduler (`posclient-sync-remainder`, hourly `0 * * * *` UTC) that fans out per tenant (every SaaS organization, or `os` with the core `TIMEZONE` config) and, for each POS config with `saveRemainder`, runs `syncRemainders` and `syncDiscounts` against that POS's products. ## Setup flow ### 1. Configure POS in Sales Create the POS in the Sales plugin UI; that record produces the POS `token` and the admin/cashier users the storefront logs in with. ### 2. Fetch the POS config From the storefront, sign in and call `posConfigsFetch(token)`. The resolver pulls the full config (POS settings, admin/cashier users, slots, product groups) from Sales through `GET /pl:sales/pos-init` (via `SERVER_DOMAIN` or the gateway) and stores a local copy in `Configs`. A failed fetch deletes the half-written config, so it is safe to retry. ### 3. Pin the device to that config `posChooseConfig(token)` sets the `pos-config-token` cookie; subsequent requests load `context.config` without re-sending the token. The `pos-auth-token` cookie from `posLogin` carries the signed-in POS user; `posLogout` clears the session. ### 4. Deploy the storefront The app reads `window.env` from `public/js/env.js`, which `docker-entrypoint.sh` regenerates from container env vars on every start; `public/js/main.js` then persists the values to `localStorage` as `pos_env_*`. For SaaS, `getEnv()` substitutes a `<subdomain>` placeholder with the request host when `NEXT_PUBLIC_APP_VERSION=SAAS`. | Variable | Purpose | | --- | --- | | `NEXT_PUBLIC_MAIN_API_DOMAIN` | Gateway base URL for the storefront's GraphQL (`/graphql` is appended) | | `NEXT_PUBLIC_MAIN_SUBS_DOMAIN` | WebSocket endpoint for `graphql-ws` subscriptions, e.g. `ws://localhost:4000/graphql` | | `NEXT_PUBLIC_SERVER_API_DOMAIN` | Server-side API base used by the SSR Apollo client (falls back to `NEXT_PUBLIC_MAIN_API_DOMAIN`) | | `NEXT_PUBLIC_SERVER_DOMAIN` | Core UI host | | `NEXT_PUBLIC_APP_VERSION` | `OS` or `SAAS` (subdomain substitution) | ## Offline and order sync The storefront is a PWA (registered by `next-pwa`, disabled in development), so the shell and catalog continue to work when the API is unreachable. Orders taken while disconnected stay local to the POS database as unsynced; a signed-in user runs `syncOrders` (Settings → sync in the app), which batches up to 100 unsynced paid orders (with items and receipt responses) and pushes them to Sales through the `pos.createOrUpdateOrdersMany` tRPC procedure. The mutation returns `{ sumCount, syncedCount }`; run it until the counts match. Hourly remainder/discount sync runs on the backend without any client involvement. ## Check it works 1. `GET <gateway>/pl:posclient/initial-setup?envs={"NEXT_PUBLIC_APP_VERSION":"OS"}` returns `success` and sets the env cookies. 2. `posConfigsFetch` creates a `Configs` row; a second call with a bad token leaves no row behind. 3. After `posChooseConfig`, `currentConfig` works without an `erxes-pos-token` header. 4. Take an order, disconnect, confirm it stays unsynced, reconnect, and run `syncOrders`. Sales shows the order once and subscriptions stream over `NEXT_PUBLIC_MAIN_SUBS_DOMAIN`. ## Troubleshooting - **`currentConfig` returns "not found"**: no `erxes-pos-token`/`pos-config-token` was sent and more than one non-deleted `Configs` row exists; the single-config fallback only applies when exactly one remains. Re-run `posChooseConfig` or delete stale configs. - **Browser requests blocked by CORS**: the storefront origin must match a regex in `ALLOWED_ORIGINS`; credentials are required, so wildcard origins do not work. - **Env changes ignored after redeploy**: `pos_env_*` values live in `localStorage`, not the bundle; clear site data or let `public/js/main.js` rewrite them from a fresh `env.js`. - **`posConfigsFetch` fails immediately**: it needs a logged-in POS user (`pos-auth-token`) and a reachable `/pl:sales/pos-init`; a partial config is rolled back, so just fix the cause and retry. --- # Operation Projects, tasks, teams, cycles, and delivery Projects, tasks, teams, cycles, milestones, triage, and a GitHub issue integration. Operation is the delivery-work plugin. Teams define statuses, cycles (sprints), and milestones; tasks carry assignees, priorities, estimate points, notes, and activity records. A per-team triage queue collects incoming work that converts into tasks. Tasks are a segment content type (`operation:task.tasks`), and other services can create tasks over tRPC: `frontline` calls `task.createFromSource` when a conversation converts into a task. For workflow automation, see [Automations & Approvals](https://erxes.io/docs/core-automations-approvals). For converting inbox conversations into tasks, see [Frontline](https://erxes.io/docs/plugin-frontline). ## Enabling and ports ```dotenv ENABLED_PLUGINS=operation ``` | Project | Role | Dev port | | --- | --- | --- | | `operation_api` | Federated GraphQL subgraph + tRPC, subscriptions, BullMQ workers | `3307` | | `operation_ui` | Module Federation remote | `3006` | [`main.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/main.ts) mounts GraphQL and the [tRPC app router](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/trpc/init-trpc.ts) (`task.*`, `fields.getFieldList`), plus an Express router for the GitHub integration endpoints (`/pl:operation/integrations/github/*`). It also registers permissions, automations, notifications, segments, properties, tags, and import/export. ## Module map | Module | Purpose | Guide | | --- | --- | --- | | `task`, `status`, `activity`, `note`, triage | Tasks, per-team status sets, notes/activity, triage queue | [Tasks & Statuses](https://erxes.io/docs/operation-tasks) | | `project`, `cycle`, `milestone` | Projects, sprints, milestone rollups | [Projects, Cycles & Milestones](https://erxes.io/docs/operation-projects-cycles) | | `team`, `template`, `fields` | Teams, membership, estimate types, task templates, custom fields | [Teams & Templates](https://erxes.io/docs/operation-teams) | | `githubIntegration`, `automations` | GitHub App config, issue sync, automation triggers/actions | [GitHub Integration & Automations](https://erxes.io/docs/operation-github-integration) | Task ids are Mongo `ObjectId`s, not generated string ids. Keep this in mind when writing tRPC callers or segment producers. ## Platform integrations - **Segments**: `operation:task.tasks` content type with filterable fields, member listing/counting, materialised `segmentIds`, and the `user.assignedTasks` / `user.createdTasks` relations. - **Automations**: segment triggers for tasks and projects plus custom completion triggers; "Create task" and "Create project" actions. See [GitHub Integration & Automations](https://erxes.io/docs/operation-github-integration). - **Import/export**: `operation:task.task` (import + export) and `operation:project.project` (export) producers. - **Notifications**: `taskAssignee`, `taskStatus`, `projectAssignee`, `projectStatus` events. - **Properties**: `task` and `project` property types with `systemFields` (`code`, `name`, `type`). - **Subscriptions**: `operationTaskChanged`, `operationTaskListChanged`, `operationProjectChanged`, `operationProjectListChanged`, `operationActivityChanged`. - **Scheduled worker**: `operations-daily-cycles-check` (hourly `0 * * * *` UTC) ends cycles that finished yesterday and starts cycles beginning today, per tenant timezone ([worker/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/worker/index.ts)). ## Configuration | Variable | Purpose | | --- | --- | | `GITHUB_APP_ID` / `GITHUB_PRIVATE_KEY` | GitHub App credentials for the issue integration | | `GITHUB_WEBHOOK_SECRET` | HMAC-SHA256 verification on `/integrations/github/webhook` | | `MONGO_URL` | Tenant MongoDB connection | | `DOMAIN` | Builds task links (e.g. automation outputs) | | `VERSION` | `saas` fans the daily cycle check out per organization | | `NODE_ENV` | Selects the compiled subscription plugin path | | `TIMEZONE` | Core config (not env): tenant timezone used by the cycle checker | - [Tasks & Statuses](https://erxes.io/docs/operation-tasks): Tasks, statuses, activities, notes, and triage - [Projects, Cycles & Milestones](https://erxes.io/docs/operation-projects-cycles): Group work into projects, time-box it with cycles, and track milestones - [Teams & Templates](https://erxes.io/docs/operation-teams): Team membership, roles, and reusable task templates - [GitHub Integration & Automations](https://erxes.io/docs/operation-github-integration): Link repositories and issues, and register operation triggers and actions --- # Tasks & Statuses Tasks, statuses, activities, notes, and triage Tasks, the per-team triage queue, statuses, notes, and activity records in the Operation plugin. Backend code lives under `backend/plugins/operation_api/src/modules/{task,status,note,activity}/`. The task module also contains the triage model and the task segment definitions in [`task/meta/segments/`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/task/meta/segments). ## Task data model `operation_tasks` ([`modules/task/db/definitions/task.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/task/db/definitions/task.ts)): | Field | Type | Notes | | --- | --- | --- | | `name`, `description` | string | `name` required | | `status` | ObjectId | references a team `Status`; required | | `teamId` | ObjectId | required; permission scope field | | `priority`, `estimatePoint` | number | `priority` defaults 0; `estimatePoint` defaults 0 | | `assigneeId`, `createdBy` | string | Core user ids (indexed, sparse) | | `labelIds`, `tagIds` | string[] | Core labels/tags | | `cycleId`, `projectId`, `milestoneId` | ObjectId | optional rollups | | `startDate`, `targetDate`, `statusChangedDate` | Date | `statusChangedDate` determines "completed" reporting | | `number`, `statusType` | number | `statusType` mirrors the status's type | | `githubIssueNumber`, `githubIssueUrl`, `githubRepoName` | | set by the GitHub sync | | `propertiesData` | mixed | `operation:task` property values, keyed by field id | | `segmentIds` | string[] | written only by the segmentation worker | > [!WARNING] > **Task _id is an ObjectId** > > `taskSchema` is a plain `new Schema(...)` without `schemaWrapper`, so `_id` stays a Mongo `ObjectId`. Segment and tRPC code always goes through the Mongoose model so string ids cast correctly. Do not bypass it with a raw driver handle. ## Statuses `Status` documents are team-scoped (`{ name, description, color, type, teamId, order }`). `type` is one of `STATUS_TYPES` ([`status/constants/types.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/status/constants/types.ts)): `STARTED=1`, `UNSTARTED=2`, `BACKLOG=3`, `COMPLETED=4`, `CANCELLED=5`, `TRIAGE=6`. Creating a team seeds the five default statuses (`backlog`, `todo`, `in progress`, `done`, `cancelled`); see `status/utils.ts`. ## Triage queue Triage items (`operation_triage`) are inbox work for a team: `{ name, description, teamId, createdBy, priority, status, number }` plus GitHub issue fields. `operationConvertTriageToTask` creates a task in the same team (optionally looking up `status` by status *type*), carries over GitHub linkage, stores the `reason` as a note on the new task, and deletes the triage record. > [!WARNING] > **operationCancelTriage is not implemented** > > `operationCancelTriage(_id)` is declared in the triage GraphQL schema but has no resolver; calling it errors at runtime. To retire a triage item, update its `status` with `operationUpdateTriage` or convert it with `operationConvertTriageToTask` (which deletes the record). ## Queries and mutations | Operation | Kind | Permission | | --- | --- | --- | | `getTasks(filter: ITaskFilter)` / `getTask(_id)` | query | `taskRead` | | `createTask(…)` / `updateTask(…)` / `removeTask(_id)` | mutation | `taskCreate` / `taskUpdate` / `taskRemove` | | `operationGetTriage(_id)` / `operationGetTriageList(filter)` | query | `triageRead` | | `operationAddTriage` / `operationUpdateTriage` | mutation | `triageCreate` / `triageUpdate` | | `operationConvertTriageToTask(_id, status: Int, reason)` | mutation | `triageConvert` | | `getStatus(_id)` / `getStatusesByType(type, teamId)` / `getStatusesChoicesByTeam(teamId)` | query | `statusRead` | | `addStatus` / `updateStatus` / `deleteStatus` | mutation | `statusCreate` / `statusUpdate` / `statusRemove` | | `getOperationActivities(contentId, …)` | query | `taskRead` | | `getNote(_id)` / `createNote` / `updateNote` / `deleteNote` | query/mutation | `noteRead` / `noteCreate` / `noteUpdate` / `noteRemove` | `ITaskFilter` accepts `status`, `priority`, `assigneeId`, `createdBy`, `cycleId`, `projectId`, `milestoneId`, `teamId`, `labelIds`, `tagIds`, `estimatePoint`, `statusType`, `cycleFilter` (`noCycle`, `anyPastCycle`, `previousCycle`, `currentCycle`, `upcomingCycle`, `anyFutureCycle`), `projectStatus`/`projectPriority`/`projectLeadId`, date bounds, and cursor pagination params. Example `createTask` call: ```graphql mutation { createTask( name: "Prepare release checklist" teamId: "team_1" status: "status_todo" assigneeId: "user_1" priority: 3 estimatePoint: 5 ) { _id name } } ``` ## Notes and activity `Note` (`{ content, contentId, createdBy, mentions, statusId }`) attaches to a task or project by `contentId`; mentions trigger a `note` notification to the mentioned users, and `removeNote` is author-only. `OperationActivity` (`{ action, contentId, module, metadata: {newValue, previousValue}, createdBy }`) is the audit trail; subscribe to `operationActivityChanged(contentId)` for live updates. ## Subscriptions `operationTaskChanged(_id)`, `operationTaskListChanged(filter: ITaskFilter)` (filtered server-side via `withFilter`), and `operationActivityChanged(contentId)` publish on create/update through `graphqlPubsub`. ## tRPC Merged in `src/trpc/init-trpc.ts`: | Procedure | Type | Notes | | --- | --- | --- | | `task.tag({ tagIds, targetIds, type, action })` | mutation | Bulk-set `tagIds` on tasks | | `task.findOne({ _ids })` | query | First task among the ids (`{ _id, name, teamId }` or `null`); non-ObjectId ids skipped | | `task.createFromSource({ userId, doc })` | mutation | Creates a task as `userId`; reads `teamId` from `doc.status` (must be a status ObjectId); publishes the task subscriptions; returns `{ _id }` | `createFromSource` is how `frontline` converts a conversation into a task. It checks **no permission**; the caller must enforce `taskCreate` for the acting user. > [!WARNING] > **"Status not found" from createFromSource** > > `doc.status` must be a status `_id` (ObjectId), not a status type number. Only `operationConvertTriageToTask` accepts a type. ## Segments and import/export - `operation:task.tasks` is a segment content type with 19 filterable fields, member listing/counting, materialised `segmentIds`, and relations `user.assignedTasks` / `user.createdTasks` (the field-joined relations rely on the `assigneeId`/`createdBy` indexes). The content type must be exactly `operation:task.tasks`; the event dispatcher keys on it. - Import type `operation:task.task` (`taskImportManage`) and export types `operation:task.task` / `operation:project.project` (`taskExportManage` / `projectExportManage`) are registered via `meta/import-export`. Repeating property groups export as numbered `<Group> <n> / <Field>` columns. - Notifications: `taskAssignee` (task assigned) and `taskStatus` (status changed). ## Permissions Modules `task`, `triage`, `note`, `status` (and `project`, `cycle`, `milestone`, `team` elsewhere) use scoped permissions: `own` (records you created), `group` (records in your teams via `scopeField: 'teamId'`), `all`. Read actions are `always: true`; write actions are grantable. `taskAssign` is a custom action for assignment control. --- # Projects, Cycles & Milestones Group work into projects, time-box it with cycles, and track milestones Projects, sprints (cycles), and milestone progress rollups in the Operation plugin. Backend code lives under `backend/plugins/operation_api/src/modules/{project,cycle,milestone}/`. ## Data models - **`operation_projects`** ([model](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/project/db/definitions/project.ts)): `{ name, description, status: Int, priority: Int, icon, teamIds: [ObjectId] (required), tagIds, startDate, targetDate, leadId, memberIds, createdBy, convertedFromId }`. `convertedFromId` links a project back to the task it was converted from; creating a second project from the same task fails with "Project has been converted already." - **`operation_cycles`**: `{ name, description, startDate, endDate, teamId, isCompleted, isActive, statistics, donePercent, unFinishedTasks: [ObjectId], number }`. - **`operation_milestones`**: `{ name, description, targetDate, projectId (required), createdBy }`. ## Queries and mutations ### Projects | Operation | Kind | Permission | | --- | --- | --- | | `getProject(_id)` / `getProjects(filter: IProjectFilter)` | query | `projectRead` | | `getProjectProgress` / `getProjectProgressByMember` / `getProjectProgressByTeam` / `getProjectProgressChart` (`_id`) | query | `projectRead` | | `getConvertedProject(convertedFromId)` | query | `projectRead` | | `cpGetProjects` | query | `forClientPortal` + `cpUserRequired`; Client Portal users only | | `createProject` / `updateProject` / `removeProject(_id)` | mutation | `projectCreate` / `projectUpdate` / `projectRemove` | `IProjectFilter` accepts `_ids`, `name`, `status`, `priority`, `teamIds`, `tagIds`, `leadId`, `memberIds`/`memberId`, `userId`, `active`, `taskId`, date bounds, and cursor params. ### Cycles | Operation | Kind | Permission | | --- | --- | --- | | `getCycle(_id)` / `getCycles(teamId, …)` / `getCyclesActive(teamId, taskId, …)` | query | `cycleRead` | | `getCycleProgress` / `getCycleProgressChart` / `getCycleProgressByMember` / `getCycleProgressByProject` (`_id`, `assigneeId?`) | query | `cycleRead` | | `createCycle(input: CycleInput)` / `updateCycle(input)` / `removeCycle(_id)` | mutation | `cycleCreate` / `cycleUpdate` / `cycleRemove` | | `endCycle(_id)` | mutation | `cycleEnd` | ### Milestones | Operation | Kind | Permission | | --- | --- | --- | | `getMilestone(_id)` / `milestones(projectId, searchValue, …)` | query | `milestoneRead` | | `milestoneProgress(projectId)` | query | `milestoneRead`; per-milestone `totalScope`, `totalStartedScope`, `totalCompletedScope` | | `createMilestone` / `updateMilestone` / `removeMilestone` | mutation | `milestoneCreate` / `milestoneUpdate` / `milestoneRemove` | ## Cycle lifecycle Cycles are per-team sprints with date boundaries: - `createCycle`/`updateCycle` reject date ranges that overlap another cycle of the same team ("New cycle overlaps with an existing cycle"), and a completed cycle cannot be updated. - `startCycle` activates a cycle only when no other cycle of that team is active. - `endCycle` snapshots `statistics` (chart data + progress by member and project), marks the cycle completed, and activates the team's next uncompleted cycle (or auto-creates one with the same duration: `Cycle <date>`, `number + 1`). Unfinished tasks move into it via `Task.moveCycle` and their ids are recorded in `unFinishedTasks` ([Cycle model](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/cycle/db/models/Cycle.ts)). - The `operations-daily-cycles-check` BullMQ scheduler (hourly `0 * * * *` UTC) runs `checkCycle` per tenant at midnight in the tenant's `TIMEZONE` core config: it ends cycles whose `endDate` fell yesterday and activates cycles whose `startDate` is today. On SaaS (`VERSION=saas`) it fans out per organization; otherwise it runs for the `os` tenant ([dailyCheckCycles.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/worker/dailyCheckCycles.ts)). > [!WARNING] > **Cycle errors** > > `startCycle` fails with "Previous cycle is active" if the team already has an active cycle; end it first. If daily rollover does not happen, `checkCycle` only acts at hour 0 in the tenant `TIMEZONE` core config, so make sure the `operations-daily-cycles-check` queue is running and the config is set. ## Progress rollups `getProjectProgress*` aggregate task scope/estimate over the project's tasks, grouped by member and team, plus a time-series chart. `getCycleProgress*` does the same per cycle with an optional `assigneeId` filter. `milestoneProgress` returns per-milestone totals (`totalScope`, `totalStartedScope`, `totalCompletedScope`) for one project. Subscriptions `operationProjectChanged(_id)` and `operationProjectListChanged(filter)` publish project updates; task updates that affect rollups publish on the task channels (see [Tasks & Statuses](https://erxes.io/docs/operation-tasks)). ## Segments and automations Projects are a declared segment content type (`operation:project.projects`). The declaration registers the content type and dependent modules but answers no field producers. Automation triggers "Project", "Project completed" (every task done), "Milestone reached", and "Team work completed" plus the "Create project" action are described in [GitHub Integration & Automations](https://erxes.io/docs/operation-github-integration). --- # Teams & Templates Team membership, roles, and reusable task templates Teams, membership, estimate scales, task templates, and custom-field plumbing in the Operation plugin. Backend code lives under `backend/plugins/operation_api/src/modules/{team,template,fields}/`. Teams are the scoping unit: statuses, cycles, triage queues, and task estimate scales all hang off a team. ## Data models - **`operation_teams`**: `{ icon, name, description, memberIds: [String], estimateType: Int, cycleEnabled: Boolean, triageEnabled: Boolean }`. - **`operation_team_members`**: `{ memberId, teamId, role }`; `role` supports `admin` / `member` / `lead` in the model but is deprecated on the GraphQL type. - **`operation_templates`**: `{ name, defaults: JSON, teamId, createdBy }`; per-team task templates whose `defaults` prefill a new task. Creating a team (`teamAdd` → [`Team.createTeam`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/team/db/models/Team.ts)) inserts `TeamMember` rows for the creator and `memberIds`, then seeds the five default statuses (`backlog`, `todo`, `in progress`, `done`, `cancelled`); see [Tasks & Statuses](https://erxes.io/docs/operation-tasks). ## Estimate scales `estimateType` is one of `TeamEstimateTypes` ([`team/utils.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/team/utils.ts)): `NOT_IN_USE=1`, `DEFAULT=2` (0–5), `FIBONACCI=3` (0,1,2,3,5,8), `EXPONENTIAL=4` (0,1,2,4,8,16). `getTeamEstimateChoises(teamId)` (sic; the operation name is misspelled in the schema) returns the `{ value, label }` list for the team's type, or `null` when estimates are off. > [!WARNING] > **Switching estimateType wipes estimates** > > `updateTeam` sets `estimatePoint: null` on every task of the team when `estimateType` changes to `NOT_IN_USE`. Other scale switches keep existing values even if they are not in the new scale. Toggling `cycleEnabled` on SaaS (`VERSION=saas`) also updates the organization's `cycleEnabled` flag, which is what the daily cycle worker filters on; see [Projects, Cycles & Milestones](https://erxes.io/docs/operation-projects-cycles). ## Queries and mutations | Operation | Kind | Permission | | --- | --- | --- | | `getTeam(_id)` / `getTeams(name, userId, teamIds, projectId, isTriageEnabled, teamId, orderBy)` | query | `teamRead` | | `getTeamMembers(teamId, teamIds)` | query | `teamRead` | | `getTeamEstimateChoises(teamId)` | query | `teamRead` | | `teamAdd(name, description, icon, memberIds)` / `teamUpdate(_id, …estimateType, cycleEnabled, triageEnabled)` | mutation | `teamCreate` / `teamUpdate` | | `teamRemove(_id)` | mutation | `teamRemove` | | `teamAddMembers(_id, memberIds)` / `teamRemoveMember(teamId, memberId)` / `teamUpdateMember(_id)` | mutation | `teamMemberManage` | | `operationTemplates(teamId)` / `operationTemplateDetail(_id)` | query | login only; no `checkPermission` | | `operationTemplateAdd(name, defaults, teamId)` / `operationTemplateEdit` / `operationTemplateRemove` | mutation | login only; no `checkPermission` | ## Custom fields `fields.getFieldList` on the plugin tRPC router returns the filterable/inputable field list for `moduleType: 'task'` or `'project'`. [`modules/fields/fieldUtils.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/fields/fieldUtils.ts) builds it from the Mongoose schemas plus hand-declared options: `priority` renders `No Priority`/`Minor`/`Medium`/`High`/`Critical` (values 0–4), `statusType` maps to the `STATUS_TYPES` labels, and `teamId`, `assigneeId`, `createdBy`, `tagIds` get selection configs (e.g. `queryName: 'users'`). Custom property values are stored on the record under `propertiesData` keyed by Core field id; the plugin meta exposes `task` and `project` property types with `systemFields` `code`/`name`/`type` shown as "Basic information" in Settings → Properties. `propertiesData` is only written through `task.createFromSource`; the task GraphQL type does not expose it. ## Permissions Team module actions: `teamRead` (always granted), `teamCreate`, `teamUpdate`, `teamRemove`, `teamMemberManage` (membership changes). Template resolvers perform no `checkPermission` call, so they fall back to the platform default (any logged-in user). Scopes are `own` (creator), `group` (member's teams), `all`. A member who cannot see a team has lost the `group` scope; re-add them with `teamAddMembers`. --- # GitHub Integration & Automations Link repositories and issues, and register operation triggers and actions The Operation plugin's GitHub App integration (issue ↔ task sync) and its automation triggers and actions. Backend code lives under `backend/plugins/operation_api/src/modules/githubIntegration/`, `src/modules/automations/`, and `src/utils/` ([`githubClient.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/utils/githubClient.ts), [`githubWebhookHandler.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/utils/githubWebhookHandler.ts)). The UI lives at `settings/operation/github` in `operation_ui`. ## Data models - **`operation_github_configs`**: links one team to one repo: `{ teamId, repoName, installationId, syncMode: 'oneWay' | 'twoWay', subdomain }`. Unique indexes on `{ teamId, subdomain }` and `{ repoName, subdomain }`. - **`operation_github_connections`**: an installed GitHub App org: `{ installationId, orgName, orgAvatarUrl, orgType, initiatedUserId, isActive, subdomain }`. Unique on `{ installationId, subdomain }`. - **`operation_github_milestone_mappings`**: maps erxes milestones to GitHub milestones per repo: `{ subdomain, installationId, repoName, erxesMilestoneId, githubMilestoneId, githubMilestoneNumber, githubMilestoneTitle }`. Tasks and triage items carry `githubIssueNumber`, `githubIssueUrl`, `githubRepoName` once linked. Triage enforces a unique sparse index on `{ githubRepoName, githubIssueNumber }`. ## Setup and authentication The integration is a **GitHub App** authenticated with `GITHUB_APP_ID` + `GITHUB_PRIVATE_KEY` (installation tokens via `getInstallationOctokit`); webhooks are verified with `GITHUB_WEBHOOK_SECRET` (HMAC-SHA256 over the raw body, `x-hub-signature-256`). - `GET /pl:operation/integrations/github/setup`: returns a small HTML page that `postMessage({ type: 'github-install-complete' })` to `window.opener` and closes the popup. Point the GitHub App's post-install redirect here. - `POST /pl:operation/integrations/github/webhook`: GitHub webhook receiver. Returns `400` without a resolvable subdomain, `401` on missing/invalid signature, `500` when `GITHUB_WEBHOOK_SECRET` is unset, `pong` on `ping`. Webhook events handled: | `x-github-event` | Behavior | | --- | --- | | `installation` | `created`/`unsuspend` upserts an active `GithubConnection`; `deleted`/`suspend` deactivates it | | `issues` `opened` | Creates a **triage** item on the configured team and appends `<!-- erxes-task-id: ... -->` to the issue body | | `issues` `opened`/`reopened`/`closed` | For an already-linked task, moves it to the team's first status of type UNSTARTED / COMPLETED / CANCELLED (`closed` + `not_planned` or `duplicate` → cancelled) | | `issues` `milestoned`/`demilestoned` | Sets/clears the task's `milestoneId` via `GithubMilestoneMapping`; only when the config's `teamId` matches the task's team | | anything else | `ignored` | Bot senders (`sender.type === 'Bot'`) are skipped to prevent sync loops. ## Outbound (two-way) sync When a team's config has `syncMode: 'twoWay'`: - `createTask` opens a GitHub issue (`POST /repos/{owner}/{repo}/issues`) with a body linking back to `https://<subdomain>.erxes.io/operation/team/<teamId>/tasks/<id>` plus the `erxes-task-id` marker, then writes the issue number/URL onto the task and pushes the milestone when `milestoneId` is set. - `updateTask` pushes status (`updateGithubIssueState` maps COMPLETED → `closed`/`completed`, CANCELLED → `closed`/`not_planned`, anything else → `open`) and milestone changes (`updateGithubIssueMilestone`, which looks up or creates the GitHub milestone by title via `resolveGithubMilestoneNumber`). GitHub failures are logged and swallowed; a task mutation never fails because the issue sync did. In `oneWay` mode only inbound (GitHub → erxes) events apply. The sync lives in the [task mutations](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/task/graphql/resolvers/mutations/task.ts). ## Queries and mutations | Operation | Kind | Permission | | --- | --- | --- | | `getGithubConfigByTeam(teamId)` / `getAllGithubConfigs(installationId)` | query | `teamRead` | | `upsertGithubConfig(teamId, repoName, installationId, syncMode)` | mutation | `teamUpdate` | | `operationGithubDisconnectTeam(teamId)` | mutation | `teamUpdate` | | `getGithubConnection` / `getGithubConnections` / `getGithubRepositories(installationId)` | query | `teamRead` | | `disconnectGithubConnection(installationId)` | mutation | `teamUpdate` | `upsertGithubConfig` validates the team exists, the installation has an active connection, the repo isn't already linked to another team, `repoName` is `owner/repo` form, and the repo is reachable through that installation (`GET /repos/{owner}/{repo}`). Re-linking a team to a different installation/repo deletes its milestone mappings; `operationGithubDisconnectTeam` does the same; `disconnectGithubConnection` refuses while any team still references the installation. ## Automations `meta/automations.ts` connects `modules/automations/` (handlers in `automationHandlers.ts`, actions in `actions/`) to the platform automation service. **Triggers** (`operationAutomationConstants.triggers`): | Trigger | Target type | Notes | | --- | --- | --- | | Task | `operation:task.tasks` | segment enrollment | | Project | `operation:project.projects` | segment enrollment | | Project completed | `operation:project.projects` (`relationType: 'completed'`) | custom; fires when every task in the project is complete | | Milestone reached | `operation:project.milestones` (`relationType: 'reached'`) | custom; milestone tasks hit the configured completion mode | | Team work completed | `operation:team.teams` (`relationType: 'completed'`) | custom; team tasks hit the completion mode | The three custom triggers support completion modes `every`/`some`/`first`/`last` (`checkCustomTrigger` in [`automationHandlers.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/modules/automations/automationHandlers.ts) evaluates task status types under the selector). **Actions** (`actions/`): | Action | Module | Notes | | --- | --- | --- | | Create task | `task` | `createTaskAction`; target source `operation:task.tasks` | | Create project | `project` | `createProjectAction`; target source `operation:project.projects` | `setProperties` supports writing fields on `tasks`, `projects`, `milestones`, and `teams` via `setProperty` rules and `setPropertyTargets`. Automation task outputs include a `DOMAIN`-based link (`/operation/tasks/<id>`). ## Environment variables | Variable | Purpose | | --- | --- | | `GITHUB_APP_ID` | GitHub App id for installation tokens | | `GITHUB_PRIVATE_KEY` | App private key (PEM) | | `GITHUB_WEBHOOK_SECRET` | Webhook signature secret; webhook 500s without it | | `DOMAIN` | Task links embedded in GitHub issue bodies and automation outputs | | `VERSION` | `saas` changes org-scoped behavior (cycle worker, org flags) | > [!WARNING] > **Sync not working?** > > If opened issues do not create triage items, no `GithubConfig` exists for that `installationId` + `repoName` + `subdomain`. If status changes do not reach GitHub, outbound sync only applies to `twoWay` configs where `githubRepoName` still matches; outbound errors are logged, not thrown. --- # Content Build websites and site content with the Content plugin: page templates, design, and CMS-powered Next.js apps. The Content plugin is the erxes CMS and website layer. It stores posts, pages, menus, categories, tags, and custom fields per site, serves them over GraphQL, and can build and deploy a rendered website through the Web Builder. ## Projects The plugin has two projects in the `erxes/erxes` monorepo: | Project | Path | Runs on | | --- | --- | --- | | `content_api` | `backend/plugins/content_api` | Plugin GraphQL service, port **3303** | | `content_ui` | `frontend/plugins/content_ui` | Module Federation remote, dev server port **3003** | `content_api` has two modules. [`src/modules/cms`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms) handles websites-as-CMS-sites, posts, pages, menus, categories, tags, custom post types, custom fields, and translations. [`src/modules/webbuilder`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder) handles Web Builder site and page records plus the Vercel deployment pipeline in `utils/utils.ts`. `content_ui` mounts its routes under `/content` and registers two entries in [`src/config.tsx`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/content_ui/src/config.tsx): `cms` at `/content/cms` and `web-builder` at `/content/web-builder`. Feature internals live in `src/modules/cms` and `src/modules/web-builder`. ## Key concepts - **A Client Portal is a website's identity.** CMS and Web Builder records carry a `clientPortalId` that ties them to one site. The portal itself is a core-api `clientportal` record, and the standalone [Client Portal](https://erxes.io/docs/app-client-portal) Next.js template (`apps/client-portal-template`) uses `cp*` operations with a portal token. - **Two operation families.** `cp*` operations such as `cpPostList`, `cpCmsPageDetail`, and `cpMenus` serve website integrations; the gateway identifies the portal from the `x-app-token` JWT and the resolvers are flagged `forClientPortal`. `cms*` operations such as `cmsPageList` and `cmsMenus` serve the admin UI, go through the permission wrapper, and require an explicit `clientPortalId`. `cp*` is not read-only; it also includes writes like `cpCmsPagesAdd`. - **Pages carry sections.** `Page.pageItems` is an ordered list of items with `name`, `type`, `content`, `order`, `objectType`/`objectId`, and a free-form JSON `config`. A front-end template maps those items to components. - **Menus have a `kind`.** The Web Builder deployer collects `kind: "main"` and `kind: "footer"` menu items to build site navigation. - **Templates live outside this repo.** `cpDeployWeb` clones `github.com/erxes-web-templates/<templateId>`, writes `data/configs.json` and `data/pages/<slug>.json` into it, and deploys to Vercel. The product repo defines the data shape; the template repositories define the rendering. ## Enable the plugin The plugin name is `content`. Enable it in `.env`, then start the dev services: ```dotenv ENABLED_PLUGINS=content ``` ```bash pnpm dev:apis # includes content_api on :3303 pnpm dev:uis # serves core-ui with the content_ui remote on :3003 ``` To run one project, use `pnpm nx serve content_api` or `pnpm nx serve content_ui`. The API gateway on port 4000 composes the plugin's GraphQL subgraph. ## Guides Pick the track that matches your role. The last card is the standalone Client Portal Next.js app, not part of the CMS or Web Builder tracks. - [For Template developers](https://erxes.io/docs/content-template-developers): Build and customize storefront templates on the Web Builder boilerplate with UI-only changes. - [For UI Designers](https://erxes.io/docs/content-ui-designers): Design web templates in Figma and map them to Web Builder sections, layouts, and pages. - [For CMS + Next.js developers](https://erxes.io/docs/content-cms-developers): Integrate the erxes CMS with Next.js: setup, data queries, rendering, and migration. - [Client Portal](https://erxes.io/docs/app-client-portal): Standalone portal backed by portal tokens --- # For Template developers Build and customize storefront templates on the Web Builder boilerplate with UI-only changes. Build and customize the front-end templates that render erxes websites: the Next.js code that turns CMS pages, sections, and menus into a deployed site. This track is for developers implementing the presentation layer of a Web Builder site: the layout, reusable sections, and page templates in a template repository such as `erxes-web-templates/ecommerce-boilerplate`. Template repositories are separate from the `erxes/erxes` product repo. What the product defines: - **The data format.** `Page.pageItems` carries section data (`type`, `content`, `order`, `objectType`/`objectId`, `config`), and `cp*` operations such as `cpCmsPageDetail` and `cpMenus` deliver it to a site ([page schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts)). - **The deploy pipeline.** `cpDeployWeb` clones your template repo, writes `data/configs.json`, `data/pages/<slug>.json`, `app/favicon.ico`, and a `next.config.ts` with erxes environment variables, then deploys to Vercel with `yarn install` and `next build` ([utils/utils.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts)). - **Seeded pages.** `createWeb` seeds slugs like `home`, `about`, `contact`, `products`, or `tours` depending on `templateType` (`ecommerce`, `restaurant`, `hotel`, `tour`) ([web.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/resolvers/mutations/web.ts)). > [!WARNING] > **UI-only scope** > > Template work is presentation-only. Adjust markup, styles, responsive behavior, and component states, but do not change the data layer: GraphQL operations, `pageItems`/`config` shapes, portal tokens, or route identifiers. Those belong to the CMS integration. ## Prerequisites - Access to the template repository you are changing. - Node.js and the package manager the template declares; deployed templates build with `yarn install`, while the erxes monorepo itself uses pnpm. - A running erxes instance with the Content plugin (`ENABLED_PLUGINS=content`) and a Client Portal if you need live data; fixture mode covers pure presentation work. - For data fetching and credentials, see the [CMS + Next.js track](https://erxes.io/docs/content-plug-cms-next-developers-introduction). ## Guides - [Introduction](https://erxes.io/docs/content-plug-template-developers-introduction): Start here to learn how to work with the erxes Web Builder boilerplate and the UI-only constraints for template developers. - [Installation](https://erxes.io/docs/content-plug-template-developers-installation): Set up your local environment for building composable, data-aware templates in the Web Builder. - [Development Guide](https://erxes.io/docs/content-plug-template-developers-development-guide): Rules and workflow for UI-only changes, repo setup, and what you are allowed to edit. - [Template Structure](https://erxes.io/docs/content-plug-template-developers-template-structure): How the ecommerce boilerplate is organized so you can update UI without touching core plumbing - [Template Layout](https://erxes.io/docs/content-plug-template-developers-template-layout): Understand the global layout (header, footer, theme) for the ecommerce boilerplate so you can adjust UI without touching core logic. - [Template Sections](https://erxes.io/docs/content-plug-template-developers-template-sections): Change how existing template sections look without breaking their data bindings - [Template Pages](https://erxes.io/docs/content-plug-template-developers-template-pages): The pages in the ecommerce boilerplate and the UI-only rule for changing them --- # Introduction Start here to learn how to work with the erxes Web Builder boilerplate and the UI-only constraints for template developers. Build and maintain the front-end templates that render erxes websites. A template provides the site's layout, reusable sections, page presentation, and the mapping between CMS content and components. ## What the product defines, and what it does not The product repository `erxes/erxes` contains the Content API/UI projects (`content_api`, `content_ui`) and a Next.js starter at [`apps/client-portal-template`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/client-portal-template). The rendered website templates live in separate repositories: the Web Builder boilerplates such as `erxes-web-templates/ecommerce-boilerplate` and `erxes-web-templates/tour-boilerplate`, and the `pages-web/web-builder` workspace. The product side still defines what passes between them: - The [Page schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts) exposes `pageItems`, where each item has `name`, `type`, `content`, `order`, `objectType`, `objectId`, and a JSON `config`. The template decides how those fields map to React components. - The [Web Builder deployer](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts) clones `github.com/erxes-web-templates/<templateId>` and writes the files the template reads: `data/configs.json`, `data/pages/<slug>.json`, `app/favicon.ico`, and a `next.config.ts` carrying the erxes environment variables. Paths such as `app/_components/sections` and helpers such as `renderSections` are boilerplate conventions, so check them in the template revision you use. ## Choose the right track - Use [CMS + Next.js](https://erxes.io/docs/content-plug-cms-next-developers-introduction) to connect a website to the product's GraphQL API, including credentials and queries. - Use this track to implement the presentation of an existing template or work within your team's Web Builder integration. - Use [Create a Plugin](https://erxes.io/docs/create-plugin) to extend the product's backend or main frontend. A website template is not a Module Federation plugin. ## Work within the agreed scope > [!WARNING] > **UI-only scope** > > For a UI-only assignment, update markup, styles, responsive behavior, and presentation states inside existing components. Preserve routes, section identifiers, configuration shapes, data loading, and authentication. Do not touch the data layer: GraphQL documents, portal tokens, or the `pageItems` fields. Adding a section, route, configuration field, or backend behavior is a separate change. Agree the scope with the owning team first. ## Follow the workflow 1. [Installation](https://erxes.io/docs/content-plug-template-developers-installation): get the repositories and run the setup. 2. [Development Guide](https://erxes.io/docs/content-plug-template-developers-development-guide): plan, implement, and check the change. 3. [Template Structure](https://erxes.io/docs/content-plug-template-developers-template-structure): trace content from its source to the rendered component. 4. [Template Layout](https://erxes.io/docs/content-plug-template-developers-template-layout): maintain shared navigation, branding, and page structure. 5. [Template Sections](https://erxes.io/docs/content-plug-template-developers-template-sections): keep section data shapes intact and handle dynamic content. 6. [Template Pages](https://erxes.io/docs/content-plug-template-developers-template-pages): check routes and complete user flows. Coordinate field and state requirements with the [designer guides](https://erxes.io/docs/content-plug-template-designers-introduction) before implementation. --- # Installation Set up your local environment for building composable, data-aware templates in the Web Builder. Run the selected template against local fixtures or an erxes instance. Complete [Local Setup](https://erxes.io/docs/local-setup) only if you also need the product backend on your machine. Template paths and editor controls depend on the separate template repository. Confirm them in your checkout; see [what the product defines](https://erxes.io/docs/content-plug-template-developers-introduction). ## Identify the repositories and toolchain Get the repository URL, branch or commit, supported Node.js version, and package manager from the template maintainer. Inspect its README, `package.json`, lockfile, and workspace configuration before installing dependencies. The existing template workflow references `pages-web/web-builder` and boilerplates such as `erxes-web-templates/ecommerce-boilerplate` and `erxes-web-templates/tour-boilerplate`. Ask the maintainer for repository access and supported branches. Do not assume a `saas-dependent` branch exists in every template. Two toolchain facts come from the product's deployer, not from the template: a deployed site builds with `yarn install` and `next build`, and the deployer patches `next` to `15.3.8` and deletes `package-lock.json` before upload. Develop against a Node and package setup that survives those constraints. See [`deploy`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L239-L255). ## Clone the selected template For example, if you have access to the ecommerce boilerplate: ```bash git clone git@github.com:erxes-web-templates/ecommerce-boilerplate.git cd ecommerce-boilerplate ``` For a builder-managed workspace, follow its instructions for placing templates under `apps/templates/`. Install at the workspace root if its workspace configuration handles dependency installation; a standalone template may install from its own root. Use the package manager and version declared by the chosen repository. The product monorepo requires pnpm, while deployed templates build with Yarn. Do not generate competing lockfiles by switching between npm, Yarn, and pnpm inside one project. ## Configure data access Copy the selected repository's example environment file if one is provided. Use its documented variable names and fill in values for your own development instance. The deployer writes a fixed set of variables into `next.config.ts` at deploy time: `ERXES_API_URL`, `NEXT_PUBLIC_ERXES_API_URL`, `ERXES_URL`, `ERXES_FILE_URL`, `ERXES_CP_ID`, `ERXES_APP_TOKEN`, `NEXT_PUBLIC_ERXES_APP_TOKEN`, `ERXES_WEB_ID`, `TEMPLATE_TYPE`, and `BUILD_MODE`/`NEXT_PUBLIC_BUILD_MODE` set to `production`, plus any `environmentVariables` stored on the Web record. Mirror the names your template reads rather than copying this list verbatim. See the [env block in `deploy`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L257-L270). For server-side CMS examples, follow [CMS Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup). Keep administrative app tokens in server-only environment variables. Do not place them in `next.config`'s `env` object for local development, `NEXT_PUBLIC_*` variables, static JSON, or client components. A public site should fetch only its intended published content. Use a fixed website endpoint and supported portal context through your server integration. Browser components can request the site's own narrowly scoped routes, as shown in [Client-side Rendering](https://erxes.io/docs/content-plug-cms-next-developers-csr). Use fixture data when available to work on presentation without live services. Check how the selected template switches between fixture, builder, and live modes. `BUILD_MODE` is written by the deployer, but how the template branches on it is the template's own convention. > [!WARNING] > **UI-only scope** > > Installation work stops at running the template and supplying its configuration. Do not edit query documents, portal token handling, or adapter logic to make a connection succeed. Fix the environment values instead. Changes to data access are out of scope for template work. ## Start the template Inspect the available scripts: ```bash node -p 'JSON.stringify(require("./package.json").scripts, null, 2)' ``` Run the documented development script with the selected package manager. ## Check it works 1. Open the URL the dev server reports. Check the shared layout, a content page, a data-backed list, and hot reload. 2. If the template talks to a live erxes instance, query the site scope directly. The [`cpPages` query](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts) resolves the portal from your `x-app-token` and lists that site's pages: ```graphql query CpPages($language: String) { cpPages(language: $language) { _id name slug status } } ``` 3. On failure, check the server logs and the browser Network panel. Fix endpoint, token, portal, or gateway configuration at its source. Do not install a CORS-disabling browser extension; use the server integration and the deployment's configured origins. ## Run the available checks Run the template's declared lint, type-check, test, and production-build scripts where present. Preview with empty content and a failed request as well as the normal data set. Record which checks ran and whether a live erxes instance was used. --- # Development Guide Rules and workflow for UI-only changes, repo setup, and what you are allowed to edit. Deliver a complete presentation change while keeping the template's data and routing intact. Template paths and editor controls depend on the separate template repository. Confirm them in your checkout; see [what the product defines](https://erxes.io/docs/content-plug-template-developers-introduction). ## Map the assignment Identify the template revision, design handoff, affected page or section, data source, and expected states. Confirm whether the assignment is presentation-only or includes new behavior. > [!WARNING] > **UI-only scope** > > For a UI-only change, preserve the existing routes, GraphQL operations, authentication, section registry, and configuration schema. Do not touch the data layer. A presentation change never requires editing a query document, a portal token, or the `pageItems` shape. Work in the relevant template: product screens in an `ecommerce` template, tour screens in a `tour` template, or room screens in a `hotel` template. The product seeds page slugs per `templateType`: `ecommerce`, `restaurant`, and `hotel` sites get `products`, `product`, `checkout`, `profile`, `confirmation`, `login`, `register`, and `booking` on top of the default `home`, `about`, `contact`, `privacy`, `terms`, `blogs`, `blog`, and `legal` pages; `tour` sites get `tours`, `tour`, `inquiry`, and the shared commerce-style pages `checkout`, `confirmation`, `profile`, `login`, `register`, and `booking`. See [`createWeb`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/resolvers/mutations/web.ts#L12-L67). Shared layout changes affect every page using that layout, so make them deliberately. ## Follow content into the component Trace a representative record through the fixture or CMS query, the template's adapter, its section mapping, and the rendered component. Compare the empty and populated versions before changing layout. A live page request returns the same shape a deploy writes to `data/pages/<slug>.json`. Fetch a page with its sections: ```graphql query CpCmsPageDetail($slug: String, $language: String) { cpCmsPageDetail(slug: $slug, language: $language) { _id name slug description coverImage status pageItems { _id name type content order objectType objectId config } } } ``` Pass either `slug` or `_id`; with neither, `cpCmsPageDetail` returns `null`. The portal comes from your token, so a slug from another site cannot be read. See [`cpCmsPageDetail`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts#L121-L138). For the other operations, see [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries). Use the [structure guide](https://erxes.io/docs/content-plug-template-developers-template-structure) to locate responsibilities. Preserve existing helpers for links, assets, localization, and formatting. Validate unknown JSON configuration before reading section-specific properties. ## Implement the design Reuse the template's own UI components and design tokens. Match the requested spacing, typography, imagery, and responsive behavior while supporting long text, missing images, and varying collection sizes. Keep controls functional: links need valid destinations, buttons need their intended action, and forms need labels, validation, errors, and submission feedback. For API-backed content, preserve loading, empty, and error states. Do not render unpublished content as a fallback when a public request returns no results. The [section catalog](https://erxes.io/docs/content-plug-template-designers-template-sections) describes design requirements for common sections. Not every boilerplate implements every section or field. ## Review with realistic content Test a narrow mobile screen, a wider layout, keyboard navigation, visible focus, and long or translated content. Exercise client-side navigation, direct page loads, reloads, and the enabled themes. Use fixture mode for predictable layout checks and the configured live integration for data checks. Check the same section in every supported rendering mode. Run the repository's available quality and build scripts from its documented workspace root. The [deployer](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts) builds with `yarn install` and `next build`, so a template that cannot pass `next build` cannot deploy. ## Prepare the handoff Describe the affected pages/components, visible change, fields kept or intentionally changed, and checks performed. Include images of the meaningful states when useful to reviewers. Call out any requirement that could not be implemented within the agreed scope. If creating a new template repository, first confirm its owner, license obligations, base revision, and destination. Keep the boilerplate remote as an upstream reference and check the destination before pushing. Do not redirect an existing project's remote as part of an ordinary UI update. --- # Template Structure How the ecommerce boilerplate is organized so you can update UI without touching core plumbing Locate the template's rendering, content, and integration responsibilities before editing it. Template paths and editor controls depend on the separate template repository. Confirm them in your checkout; see [what the product defines](https://erxes.io/docs/content-plug-template-developers-introduction). ## Files the deployer generates Part of the structure is not a convention at all. The Web Builder deployer writes these files into the template clone before every Vercel deploy, and a template must be able to read them: | Generated file | Contents | | --- | --- | | `data/configs.json` | Site identity and theme: `cpId`, `templateId`, `templateType`, `meta` (title, description, logo, favicon, keywords, author, url), `appearance` (theme, fonts, colors), `menus` (`main`, `footerMenu`), and `additional` (copyright, social links, integrations) | | `data/pages/<slug>.json` | Per-page `title`, `description`, `coverImage`, and `pageItems`. The slug `home` (or `/`) is written as `index.json` | | `next.config.ts` | A generated config carrying the erxes env block (`ERXES_API_URL`, `ERXES_APP_TOKEN`, `ERXES_CP_ID`, `ERXES_WEB_ID`, `TEMPLATE_TYPE`, `BUILD_MODE`, and custom `environmentVariables`) | | `app/favicon.ico` | The site's favicon, downloaded from the erxes file endpoint | See [`buildConfigs` and `buildPageConfigs`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L12-L91) and the [file-writing block](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L276-L332). The `app/` path also tells you the deployed templates are Next.js App Router projects. ## Common folder responsibilities Earlier ecommerce template documentation used this layout as a reference. Inspect the selected repository rather than creating missing files just to match the tree. ```text app/ layout.tsx Shared page layout page.tsx Home route _components/ Header.tsx Navigation and branding Footer.tsx Shared footer sections/ Reusable page sections ... Content and application routes components/ Template UI primitives and shared components data/ configs.json Generated site config (and fixture stand-in) pages/ Generated per-page content (and fixtures) graphql/ Operations grouped by feature lib/ Fetchers, adapters, contexts, rendering helpers types/ Template and section types public/ Static assets ``` Helpers named `ApolloWrapper`, `ClientLayout`, `usePage`, and `renderSections` may exist in these templates. Their names, locations, and behavior are template-specific. Keep server-only credential handling outside any browser import graph. > [!WARNING] > **UI-only scope** > > Reading the structure is not a license to change it. For a UI-only assignment, treat `graphql/` documents, `lib/` fetchers, and the generated `data/` shapes as fixed inputs. Change presentation components, not the data layer. ## Content flow A typical content page follows these steps: 1. A fixture or server-side CMS request supplies page data. Live sites use `cpCmsPageDetail` (or the generated `data/pages/*.json`); the `pageItems` list is the section input. 2. An adapter validates and converts the response into the template's expected shape. 3. The page renderer maps each item's `type` to an existing component. 4. Each component renders validated `content` and `config` with theme and layout values. The product [Page schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts) defines `pageItems` with `_id`, `name`, `type`, `content`, `order`, `objectType`, `objectId`, and JSON `config`. Web Builder pages use the same structure, except their items carry `contentType`/`contentTypeId` instead of `objectType`/`objectId`; see the [WebPage schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/schemas/webPage.ts). The schema does not define React component names or validate a template-specific Hero configuration. Those are template decisions. ## Find the configuration `data/configs.json` keys such as `appearance`, `meta`, and `menus` are product-written: the deployer generates them from the Web record's `appearances`, `externalLinks`, `integrations`, and `kind: "main"`/`kind: "footer"` menu items. Check the keys and the data-loading code your template uses before relying on them. Keep public configuration and secret credentials separate. Static JSON and client components are visible to visitors. Use the [CMS Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup) for server-side authentication and [CMS Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries) for the response shapes. ## Before changing a shared key Find every consumer of a section identifier, configuration key, or page slug, including fixtures and editor previews. A presentation change should keep them intact. A planned change must update its producers, validators, renderers, and stored content together. Anything written by the deployer is fixed by the product, so a template cannot rename generated keys on its own. --- # Template Layout Understand the global layout (header, footer, theme) for the ecommerce boilerplate so you can adjust UI without touching core logic. Maintain the shared shell that wraps the template's pages: branding, navigation, content width, typography, and footer. Template paths and editor controls depend on the separate template repository. Confirm them in your checkout; see [what the product defines](https://erxes.io/docs/content-plug-template-developers-introduction). ## Locate the layout In the App Router convention, start with `app/layout.tsx`, shared Header/Footer components, the client layout wrapper if present, and global styles. Check which pages share the shell and which application routes have a separate layout. > [!WARNING] > **UI-only scope** > > Preserve providers and server/client boundaries during presentation work. A visual Header change should not require exposing a token to the browser or moving page data loading into a client component. Do not touch the data layer. ## Use existing theme values Use the template's existing font, color, spacing, and appearance values. In a deployed site these come from `data/configs.json`, which the deployer generates with a fixed shape: `meta` (title, description, logo, favicon, keywords, url), `appearance` (`theme`, `baseFont`, `headingFont`, `baseColor`, `backgroundColor`), `menus` (`main` and `footerMenu` arrays), and `additional` (copyright, social links, integrations). See [`buildConfigs`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L12-L77) and the [`Web` schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/schemas/web.ts) it reads from. Confirm which of those keys your template reads before depending on them. Provide useful behavior for missing logos, long site names, optional contact details, and translated menu labels. Keep navigation labels and destinations aligned with the content source. Render only the authentication and language controls supported by the selected template. ## Header and footer behavior The header should make the main navigation and current location understandable on small and large screens. Keep menu toggles keyboard-operable, expose their expanded state, and preserve visible focus. The footer may include secondary navigation, contact information, social links, copyright, and legal links. The deployer maps `kind: "main"` menu items to `menus.main` and `kind: "footer"` items to `menus.footerMenu`, each with `label`, `url`, `icon`, `parentId`, and `order`. Each rendered link needs a valid destination; do not use an empty link or `#` as a substitute for unavailable content. For live navigation, `cpMenus` returns the site's items filtered by `kind`: ```graphql query CpMenus($kind: String, $language: String, $webId: String) { cpMenus(kind: $kind, language: $language, webId: $webId) { _id parentId label linkType kind icon url order openInNewTab target linkedContent { _id title slug linkType } } } ``` Branch on `linkType` (`URL`, `PAGE`, `POST`, `CATEGORY`, `TAG`) and use `linkedContent.slug` to build hrefs without a second request; see the [MenuItem schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/menu.ts). For UI-only work, keep menu fetching, authentication, and metadata logic intact. If data is missing or a destination is unsupported, work out the content or scope with the owner rather than inventing a flow. ## Check the shared change Check the homepage, a content detail page, and any affected account or checkout page. Test both available themes, mobile navigation, long labels, missing optional fields, and keyboard focus. A fixed header should not hide anchored content or focused controls. Run the template's available checks and production build, then review the [page-level behavior](https://erxes.io/docs/content-plug-template-developers-template-pages). --- # Template Sections Change how existing template sections look without breaking their data bindings A section is a reusable part of a page (a Hero, article list, gallery, or contact form) rendered from one entry in the page's `pageItems` list. Its presentation must stay compatible with the content the template receives. Template paths and editor controls depend on the separate template repository. Confirm them in your checkout; see [what the product defines](https://erxes.io/docs/content-plug-template-developers-introduction). ## Separate the CMS data shape from the component's props The [Page schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts) exposes `PageItem.type` and a JSON `config`. It does not constrain `config` to a particular Hero or gallery schema, so a template must validate that data before treating it as typed component props. On Web Builder pages the same items are `WebPageItem`s carrying `contentType`/`contentTypeId` instead of the CMS `objectType`/`objectId`; the deployer copies each item into `data/pages/*.json` under the `objectType`/`objectId` key names. See [`buildPageConfigs`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L79-L91). For example, a template might define these component types: ```ts type HeroConfig = { title: string; description?: string; imageUrl?: string; imageAlt?: string; primaryCta?: { label: string; href: string }; }; type HeroProps = { config: HeroConfig; }; ``` These names are illustrative template types, not GraphQL fields guaranteed by erxes. At the data boundary, validate unknown configuration using the selected template's established validation approach before passing it to the component. Avoid a broad `any` type that hides missing or incompatible values. ## Preserve registration and identity Locate the mapping from the item's `type` to a component, including any separate fixture, preview, and CMS renderers. Common locations include `app/page.tsx`, `lib/renderSections.tsx`, and `lib/usePage.tsx`, but your template may differ. > [!WARNING] > **UI-only scope** > > For a UI-only assignment, change the existing component's presentation and keep its identifier, inputs, and data hooks intact. Do not rename a `pageItems[].type` value or alter the fetch that supplies it. For a planned new section, update every supported renderer, editor definition, validator, and fixture together. ## Render every state Support missing optional text, long headings, empty collections, and failed requests. Use the existing asset and route helpers. Give images appropriate alternative text and keep links valid when content is unavailable. A section that lists linked records (for example, a `type` that references posts through `objectType`/`objectId` or `contentType`/`contentTypeId`) needs a loading state, an empty state, and a bounded list. Keep data fetching and secret credentials on the server where required; a section needing browser state can receive a safe, serialized data subset. Follow the [CMS rendering examples](https://erxes.io/docs/content-plug-cms-next-developers-ssr) for integration behavior. For builder-managed pages, `cpWebPage` returns the same section list scoped to a `webId` (see the [WebPage schema](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/schemas/webPage.ts)): ```graphql query CpWebPage($slug: String, $webId: String, $language: String) { cpWebPage(slug: $slug, webId: $webId, language: $language) { _id name slug pageItems { _id name type content order contentType contentTypeId config } } } ``` ## Check the section Use a known fixture, an empty fixture, and representative live content. Check the component in every supported rendering mode and on more than one page if it is shared. Check responsive layout, keyboard controls, image sizing, and loading/error feedback. Use the [designer section catalog](https://erxes.io/docs/content-plug-template-designers-template-sections) to agree on editable fields and states, then continue with [Template Pages](https://erxes.io/docs/content-plug-template-developers-template-pages). --- # Template Pages The pages in the ecommerce boilerplate and the UI-only rule for changing them A page combines route handling, data loading, layout, and section rendering. Check the complete user flow when changing its presentation. Template paths and editor controls depend on the separate template repository. Confirm them in your checkout; see [what the product defines](https://erxes.io/docs/content-plug-template-developers-introduction). ## Identify the page type - Content pages include Home, About, Contact, articles, and policy pages. Their content may come from CMS records or fixtures. - Collection and detail pages display records such as posts, products, tours, or rooms. They need explicit loading, empty, missing-record, and error behavior. - Application pages include login, profile, checkout, payment, and confirmation. They depend on authentication or transaction flows as well as layout. The product seeds a fixed slug set when a Web Builder site is created: `home`, `about`, `contact`, `privacy`, `terms`, `blogs`, `blog`, and `legal` for every site; `products`, `product`, `checkout`, `profile`, `confirmation`, `login`, `register`, and `booking` for `ecommerce`, `restaurant`, and `hotel` types; `tours`, `tour`, and `inquiry` on top of the commerce-style set for `tour`. See [`createWeb`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/resolvers/mutations/web.ts#L12-L67). Earlier ecommerce conventions map these to routes such as `app/products/page.tsx`, `app/blog/[id]/page.tsx`, and `app/profile/page.tsx`. The selected template defines which routes exist and whether they use an ID, slug, or other parameter. Builder editability is also template-specific. ## Keep route and data behavior aligned > [!WARNING] > **UI-only scope** > > For a UI-only change, preserve the route identifier, query variables, pagination, portal context, and section mapping. Inspect the existing `usePage` or server fetcher rather than guessing that a route name maps directly to a CMS slug. Do not touch the data layer. Use `cpPageList` to list a live site's routes (for example to feed `generateStaticParams`), and `cpCmsPageDetail` to fetch a single page: ```graphql query CpPageList($language: String, $limit: Int, $cursor: String) { cpPageList(language: $language, limit: $limit, cursor: $cursor) { totalCount pageInfo { hasNextPage endCursor } pages { _id name slug type status updatedAt } } } ``` Use the [CMS query reference](https://erxes.io/docs/content-plug-cms-next-developers-queries). Treat a missing record differently from a network or authorization error. `cpCmsPageDetail` returns `null` when neither `_id` nor `slug` matches (see the [page queries](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts)). Do not render unrelated content when the requested slug is unavailable. ## Check the full page 1. Open the URL directly and reload it. 2. Navigate to it through the site and back through browser history. 3. Test an empty list, a missing record, long content, and a failed request. 4. Check metadata, links, images, and pagination for the selected record. 5. Test authentication and submission behavior for application pages affected by the change. 6. Check fixture/preview mode and live mode if both are supported. Run the available build and quality checks. Describe any route or data change separately from visual changes in the handoff. Coordinate editable content and fixed application flows with [Template Pages for Designers](https://erxes.io/docs/content-plug-template-designers-template-pages). --- # For UI Designers Design web templates in Figma and map them to Web Builder sections, layouts, and pages. Design web templates in Figma and hand them off so a developer can map them to the erxes CMS and Web Builder: site-wide branding, navigation menus, pages, and the ordered sections inside each page. This track is for designers: you do not need to write code, but you need to know what the CMS stores so your design names editable fields instead of imaginary ones. ## What you deliver - Desktop and mobile layouts for each page type. - Shared tokens: colors, fonts, spacing, and image treatments. - A page-by-page inventory of sections, in order, with editable fields marked. - States for dynamic content: loading, empty, error, long text, and missing images. ## How your work maps to the product | Your design element | Where it lives in erxes | | --- | --- | | Site name, logo, favicon, domain, copyright | `Web` fields (`name`, `logo`, `favicon`, `domain`, `copyright`) | | Brand colors and fonts | `Web.appearances` (`primaryColor`, `secondaryColor`, `accentColor`, `backgroundColor`, `fontSans`, `fontHeading`, `fontMono`) | | Header and footer navigation | `MenuItem` records with `kind` `main` and `footer` | | Each page and its route | `Page` records (`name`, `slug`, `description`, `coverImage`, `status`) | | Sections inside a page | `Page.pageItems`; each with `type`, `order`, `content`, and a `config` object | | Blog and listing cards | `Post` fields (`title`, `excerpt`, `thumbnail`, `categories`, `tags`, `publishedDate`) | Section `type` values and the keys inside `config` are defined by the selected template repository, not by the CMS schema. The Web Builder deploys a site by writing your page sections into per-page JSON files that the template renders. Confirm the available section types with the template developer before designing around them. ## Guides - [Introduction](https://erxes.io/docs/content-plug-template-designers-introduction): How designers create, customize, and hand off web templates from Figma to the Web Builder - [Template Structure](https://erxes.io/docs/content-plug-template-designers-template-structure): How components, sections, and pages fit together in a Web Builder template - [Template Layout](https://erxes.io/docs/content-plug-template-designers-template-layout): Layout, color, and font options a template exposes to the Web Builder - [Template Sections](https://erxes.io/docs/content-plug-template-designers-template-sections): Section types available in the Web Builder and the fields each one exposes - [Template Pages](https://erxes.io/docs/content-plug-template-designers-template-pages): Page types the Web Builder supports, grouped by how much editors can change --- # Introduction How designers create, customize, and hand off web templates from Figma to the Web Builder Design reusable website layouts and sections that map to what the erxes CMS stores and what the selected Web Builder template can render. The product repository defines what data the CMS stores. The section types, editor controls, and page rendering live in a separate template repository per website template, so this guide gives you a design workflow, not a guarantee that every template supports every control. Review the selected template with its developer before preparing the handoff. ## Start with what the CMS stores Every visual element in your design ends up in one of these records. Naming the CMS field in your handoff is the fastest way to get an accurate implementation. - **The website** ([`Web`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/schemas/web.ts)): `name`, `description`, `domain`, `copyright`, `logo`, `favicon`, plus `appearances`: `backgroundColor`, `primaryColor`, `secondaryColor`, `accentColor`, `fontSans`, `fontHeading`, and `fontMono`. Social links are stored in `externalLinks` as name/URL pairs. These are the branding settings you control; a Web Builder deployment writes them into the site's `appearance` and `meta` configuration. - **Navigation** ([`MenuItem`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/menu.ts)): `label`, `url`, `icon`, `order`, and `parentId` for nesting. The deploy step looks for menus with `kind` `main` (header) and `footer`; use those names in your handoff. Items can also carry `linkType` (`URL`, `PAGE`, `POST`, `CATEGORY`, `TAG`), `openInNewTab`, and `target`. - **Pages** ([`Page`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts)): `name`, `slug` (the route), `description`, `coverImage`, `status`, and `pageItems`. A page's sections are the `pageItems` array. Each item has a `type` (which section component renders it), an `order`, a free-form `config` object for the section's options, a `content` field, and optional `objectType`/`objectId` pointers that link the section to another record. - **Posts** ([`Post`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts)): `title`, `slug`, `excerpt`, `content`, `thumbnail`, `images`, `categories`, `tags`, `authorId`/`authorKind`, `publishedDate`, and `status` (`draft`, `published`, `scheduled`, `archived`). Design your cards and article pages against these fields. Section `type` values and the keys inside `config` are defined by the template, not the CMS schema. The CMS stores any `type` string and any `config` JSON; whether a "hero" or "gallery" section renders correctly depends on the template's components. ## Design for realistic content Identify the audience, primary page action, and supported routes. Use realistic titles, images, translated text, and collection sizes. The CMS supports per-language translations of post and page fields, so check how your layout handles longer translated strings. Include loading, empty, error, and success states wherever the page renders live data such as post lists or menus. ## Handoff checklist | Design element | CMS field it maps to | What to specify | | --- | --- | --- | | Brand colors | `Web.appearances.primaryColor`, `secondaryColor`, `accentColor`, `backgroundColor` | Hex values and which UI role each color plays | | Typography | `appearances.fontSans`, `fontHeading`, `fontMono` | Font family and fallback for body, headings, code | | Logo and favicon | `Web.logo`, `Web.favicon` | Source assets and minimum sizes | | Header nav | `MenuItem` with `kind` `main` | Item order, nesting depth, mobile behavior | | Footer nav and copyright | `kind` `footer`, `Web.copyright` | Link groups, legal text | | Social links | `Web.externalLinks` | Name/URL pairs and icons | | Page list | `Page.name`, `Page.slug`, `Page.status` | Route for each page | | Sections per page | `Page.pageItems` (`type`, `order`, `config`) | Ordered section list and editable fields per section | | Post cards and article | `Post` fields above | Which fields appear, image crops, date format | ## Follow the design track 1. [Template Structure](https://erxes.io/docs/content-plug-template-designers-template-structure): organize global elements, pages, sections, and fields. 2. [Template Layout](https://erxes.io/docs/content-plug-template-designers-template-layout): specify branding, header, and footer. 3. [Template Sections](https://erxes.io/docs/content-plug-template-designers-template-sections): define section content and states. 4. [Template Pages](https://erxes.io/docs/content-plug-template-designers-template-pages): document per-page editability and flows. --- # Template Structure How components, sections, and pages fit together in a Web Builder template Organize the design around the parts that change together: site-wide settings, navigation, pages, and the ordered sections inside each page. ## Establish the hierarchy The CMS hierarchy in erxes maps cleanly onto how designers already break a site into parts: - **Site (global) elements**: stored once on the [`Web`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/schemas/web.ts) record: name, domain, copyright, logo, favicon, brand colors and fonts under `appearances`, social links under `externalLinks`, and analytics IDs under `integrations`. - **Navigation**: [`MenuItem`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/menu.ts) records grouped by `kind`. The Web Builder [deploy step](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L133-L185) collects `kind` `main` and `kind` `footer` menus; nesting comes from `parentId`, order from `order`. - **Pages**: [`Page`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts) records. `name` is the title, `slug` is the route, `status` controls availability, and `coverImage` supplies a social/hero image. - **Sections**: entries in `Page.pageItems`. Each item has a `type` string (which component the template renders), an `order`, a `config` JSON object for options, a `content` field, and optional `objectType`/`objectId` pointers to a linked record such as a post or category. - **Fields**: the editable values inside `config` and `content`. Their names and types are defined by the template's section components, not by the CMS schema. Confirm the available section `type` values and `config` keys in the selected template repository. A Figma component does not automatically create a matching section type or editable field. ## Map content to the design Mark which values are global (on `Web`), page-specific (on `Page`), section-level (in `pageItems`), or supplied by a collection (posts, categories, menus). Distinguish labels and placeholder examples from required production content. Editors replace your placeholder copy with real text, so design for the longest realistic value. For repeated content such as post cards or gallery images, show zero, one, and several items, plus the longest realistic title or label. Define how missing images and optional text affect spacing: `Post.excerpt`, `Post.thumbnail`, and `Page.description` can all be empty, so a missing value is a normal case, not an edge case. For dynamic content, specify loading and error states as well as the successful layout. ## Specify responsive behavior Describe how navigation, columns, spacing, image crops, and content order change as the available width changes. Use the template's agreed breakpoints and tokens. Check reading order and keyboard access when desktop elements move on mobile, especially for the `main` menu, which collapses into a mobile menu in most templates. Accessibility and responsiveness need implementation and testing; they are not guaranteed by installing a template. Include visible focus, readable text contrast against your `appearances` colors, meaningful labels, and alternatives for essential image content in the handoff. ## Handoff checklist | Decision | Where it is stored | Open question for the developer | | --- | --- | --- | | Which elements are global vs. per-page | `Web` vs. `Page`/`pageItems` | Any element that looks global but must vary per page? | | Section inventory | `Page.pageItems[].type` | Which `type` values does the template implement? | | Editable options per section | `Page.pageItems[].config` | Which keys are editable, and their defaults? | | Linked content in sections | `objectType`/`objectId` | Which object types does each section accept? | | Collection-driven lists | `Post`, `PostCategory`, `MenuItem` | Source query, ordering, and maximum items | | Breakpoints and tokens | Template theme (repo-specific) | Breakpoint names and token mapping | ## Review with the developer Confirm the field mapping, component reuse, editable areas, missing-content behavior, and any new capability the design needs. Settle changes to the data shape (new `config` keys, new section types, new linked objects) before marking the design ready. --- # Template Layout Layout, color, and font options a template exposes to the Web Builder Specify the shared visual system and navigation behavior once, then apply it consistently to every page using the layout. ## Theme and branding The website's brand settings live on the [`Web`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/graphql/schemas/web.ts) record. `Web.appearances` stores the tokens a deployment applies: `primaryColor`, `secondaryColor`, `accentColor`, `backgroundColor`, `fontSans`, `fontHeading`, and `fontMono`. The [deploy step](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L11-L70) maps them into the template's site config: `fontSans` becomes the base font, `fontHeading` the heading font, `primaryColor` the base color, and `backgroundColor` the page background. Define your palette as semantic roles (primary action, background, body text, accent, focus indicator) and map each role to one of those fields. If your design needs a token the list does not cover (say, a separate surface color or a fourth font), flag it: that requires a template-level change, not a CMS edit. `Web.logo` and `Web.favicon` hold the brand assets; supply production-ready files. ## Header The header is built from the `main` menu: [`MenuItem`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/menu.ts) records with `kind` `main`. Each item carries a `label`, `url`, `icon`, `order`, optional `parentId` for dropdown nesting, `openInNewTab`, `target`, and a `linkType` (`URL`, `PAGE`, `POST`, `CATEGORY`, `TAG`) that determines what the item links to. Design the logo/site name, the primary navigation, the mobile menu, and any account or language controls the template supports. Define selected, hover, focus, and expanded states. Show the behavior for a long site name, long menu labels, and a menu deeper than the template supports; agree on the maximum nesting depth with the developer. Keep the primary action clear and the mobile reading order understandable. Specify whether the header scrolls away or stays fixed, and how anchored content remains visible beneath it. ## Footer The footer uses the `footer` menu (`kind` `footer`) plus `Web.copyright` for the legal line. Social links come from `Web.externalLinks`, a name/URL map; specify which networks, their icons, and their order. Include the supported secondary navigation, contact information, and policy links. Every link needs an intended destination. If a field is optional, show the layout with that field absent. `copyright`, social links, and secondary menus can all be empty. Avoid assuming login, checkout, or language-switching features are available just because their buttons appear in the design. Confirm the implemented route and behavior first; analytics and messenger integrations exist (`Web.integrations` holds Google Analytics, Facebook Pixel, Google Tag Manager, and a messenger brand code), but visible features such as search or cart depend on the template. ## Handoff checklist | Element | Stored in | Confirm with the developer | | --- | --- | --- | | Color roles | `Web.appearances.primaryColor`, `secondaryColor`, `accentColor`, `backgroundColor` | Which role maps to which field; any missing token | | Fonts | `appearances.fontSans`, `fontHeading`, `fontMono` | Loading method and fallbacks | | Logo, favicon | `Web.logo`, `Web.favicon` | Sizes, formats, dark-mode variant | | Header navigation | `MenuItem` `kind` `main` | Max depth, icons, mobile collapse | | Footer navigation | `MenuItem` `kind` `footer` | Grouping and column layout | | Copyright line | `Web.copyright` | Text and whether year is dynamic | | Social links | `Web.externalLinks` | Supported networks and icons | | Tracking/messenger | `Web.integrations` | Which IDs the deployment needs | ## Handoff and review Provide shared components and variants, token mappings, mobile behavior, and examples of long or missing content. Review the layout on a content page and an application page if both use it. --- # Template Sections Section types available in the Web Builder and the fields each one exposes Agree on each section's content, behavior, and states. The CMS stores sections as `Page.pageItems` entries; which `type` values and `config` keys exist is decided by the selected template repository, not by the schema. ## Shared section requirements Every section in your design becomes a `pageItems` entry with these stored fields (see the [`PageItem` fields](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts#L4-L13)): - `type`: a string naming the section component the template renders. - `order`: the section's position on the page. - `config`: a free-form JSON object holding the section's options and editable values. - `content`: a text payload, where the section uses one. - `objectType`/`objectId`: optional pointers linking the section to another record (for example a specific post or category). Their meaning is template-defined. For each section, document its purpose, editable fields, required and optional content, image treatment, destinations, and responsive behavior. Use the exact `type` value and `config` keys from the selected template when handing off implementation details. Show long text, missing optional fields, and relevant loading, empty, error, and success states. Keep `type` strings and `config` keys stable during a presentation-only change. ## Promotional and editorial sections These are common patterns, not a list of components guaranteed to exist in every template: - Hero: headline, supporting text, background or featured image, primary action, and optional secondary action. Show image crop and text behavior on mobile. - Banner: a focused message with an optional image and action. Define how it works without a background image. - Image with text / About: heading, body copy, image, and optional link. Specify reading order on narrow screens. - Text: heading, body content, and optional action. Show headings, lists, links, and long text if supported. - Gallery: title, description, and images. Define captions, alternative text, cropping, empty state, and any supported viewer interaction. - Video: heading, introduction, supported video URL, and a poster or fallback. A `Post` can also carry `videoUrl`, `video`, and `audio` attachments. Show loading or unavailable-media behavior. - Carousel: slide content, actions, and controls. If automatic movement is supported, define pause and manual navigation behavior. ## Forms and contact sections Forms need field labels, instructions, required indicators, validation messages, submission progress, success confirmation, and a recoverable failure state. Include the intended destination of the submission so the developer can check the full flow. A contact section may combine an inquiry form, address, phone, email, social links, and a map. Show the layout without optional fields and provide a useful alternative when the map cannot load. Site-level social links belong in `Web.externalLinks`; page-specific contact details belong in the section's `config`. A booking or reservation section depends on an implemented integration in the template. Agree on dates, guest counts, availability, pricing, validation, and confirmation behavior with the owning team; these are not generic CMS page fields. ## Lists and personalized content Post, category, product, and other collection lists need card content, image treatment, ordering, destination links, pagination or expansion behavior, and empty results. Build cards from real fields: - [`Post`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L41-L84): `title`, `slug`, `excerpt`, `thumbnail`, `publishedDate`, `categories`, `tags`, `viewCount`, and `authorId`/`authorKind`. - `PostCategory`: `name`, `slug`, `description`, `parentId`. - `PostTag`: `name`, `slug`, `colorCode`. Publication time and record-creation time are different fields: `publishedDate` vs. `createdAt`. Choose the intended one for the card. A `Post.author` union to the core `User` type exists, but what a template displays as a byline depends on its own implementation; agree on it with the developer. For recently viewed or personalized sections, define the first-visit state and the no-history state. Whether personalization exists is determined by the template and its integrations, not the CMS. ## Handoff checklist | Per section | Stored in | Specify | | --- | --- | --- | | Section identity | `pageItems[].type` | Exact `type` string the template expects | | Position | `pageItems[].order` | Where it sits on the page | | Editable options | `pageItems[].config` | Each key, its type, default, and constraints | | Text payload | `pageItems[].content` | Format: plain text, HTML, or markdown | | Linked record | `objectType`/`objectId` | What the section may point at | | Dynamic states | Template component | Loading, empty, error, long-content variants | ## Section handoff For each section, include a populated example and its alternate states, the editable field mapping, shared tokens, media assets, and responsive variants. Flag new fields or interactions that require development. A new `config` key is cheap to store but still needs a component that reads it. --- # Template Pages Page types the Web Builder supports, grouped by how much editors can change Document which parts of each page editors can change and which parts belong to a fixed application flow. In erxes, a page is a [`Page`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts) record. What editors can change is its fields plus the ordered `pageItems` you designed as sections. ## Content pages Home, About, Contact, articles, policies, and custom content pages map to `Page` records: `name` is the title, `slug` is the route, `description` and `coverImage` feed SEO and hero areas, `status` controls whether the page is live, and `pageItems` holds the sections. On deploy, each page is written to a per-page JSON file named after its `slug` (see the [deploy step](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L314-L321)). A page whose slug is `/`, empty, or `home` becomes the index page. Keep slugs short, lowercase, and stable; changing a slug changes the route. Confirm with the developer which operations editors get: change text and images inside `config`/`content`, reorder `pageItems`, add or remove sections, or edit page-level fields. List the supported operations per page rather than assuming all pages are equally editable. For collection and detail pages (a post archive, a category page, a product detail), specify the data source, card/detail fields, links, pagination, empty results, and missing-record behavior. These pages render live queries. A `Post` detail needs `title`, `content`, `publishedDate`, `thumbnail`, `categories`, `tags`, and an agreed author display; an archive needs the same card fields plus ordering and pagination rules. ## Application pages Login, registration, password reset, profile, checkout, payment, and confirmation depend on functional workflows in the template. They may share your branding (colors, fonts, logo from `Web.appearances`/`Web.logo`) while keeping the interaction and validation logic fixed. Do not describe them as universally editable or non-editable. Confirm the available customization boundary and design within it. Show submission progress, validation errors, authentication failures, and completion states where relevant. ## Page specification For each page, hand off: - Its purpose, `slug` (route), and primary action. - Shared layout and the ordered `pageItems` list with each section's `type`. - Editable fields: which `config` keys, `content` values, and page fields editors control. - Mobile and wide-screen layouts. - Loading, empty, error, and success behavior for dynamic sections. - Navigation into the page (which `main`/`footer` menu items point to it) and the next step in the flow. ## Handoff checklist | Per page | Stored in | Specify | | --- | --- | --- | | Page title | `Page.name` | SEO title and any separate display title | | Route | `Page.slug` | Final URL; `home`/`/` becomes the index page | | Metadata | `Page.description`, `Page.coverImage` | Social card text and image | | Visibility | `Page.status` | Live vs. hidden during editing | | Sections | `Page.pageItems` (`type`, `order`, `config`) | Ordered list with editable fields per section | | Menu entries | `MenuItem` pointing at the page | Label, `kind`, `linkType`, order | | Bound content | `objectType`/`objectId`, linked posts/categories | Which records feed dynamic sections | Review direct entry, navigation, and the mobile flow with the developer. Link requirements that need new routes, fields, or integrations to the agreed development scope. --- # For CMS + Next.js developers Integrate the erxes CMS with Next.js: setup, data queries, rendering, and migration. Integrate the erxes CMS with a Next.js application: environment setup, querying CMS content over GraphQL, server- and client-side rendering, and migrating an existing integration. This track is for developers running (or building) a Next.js website that displays posts, pages, menus, categories, and tags managed in erxes. The guides assume the App Router and native `fetch`; the same GraphQL documents work with Apollo Client or another GraphQL client. ## Prerequisites - An erxes instance with the **Content** plugin enabled (see [Local setup](https://erxes.io/docs/local-setup) or [Self-hosting](https://erxes.io/docs/self-hosting)). - A **Client Portal** created for your website, with its CMS records (posts, pages, menus) associated to that portal. - The portal's token and your deployment's gateway URL (for example `https://YOUR_TENANT.app.erxes.io/gateway/graphql`, or `http://localhost:4000/graphql` in a local setup). ## How authentication works The website's server sends the Client Portal token as the `x-app-token` header on every GraphQL request. The gateway verifies the JWT and identifies the portal named by its `clientPortalId` claim; the token identifies **which website** is asking, and every `cp*` operation is scoped to that portal automatically. A separate `client-auth-token` identifies a signed-in portal user, and an `erxes-app-token` authenticates an application principal for administrative work. Keep all of these on the server; none belongs in browser code or `NEXT_PUBLIC_*` variables. ## Two operation families - `cp*` operations serve client-portal integrations. They derive the portal from `x-app-token`, so a public website only ever sees its own content. - `cms*` operations serve the admin UI and authenticated tooling. They need a team-member session or an Apps token, and most require an explicit `clientPortalId` argument. The `cp*` prefix is not a read-only or published-only guarantee: `cpPost` returns a record regardless of status and there are `cp*` mutations. The guides show where to enforce `status: published` and how to limit the browser to a fixed endpoint. ## Guides - [Introduction](https://erxes.io/docs/content-plug-cms-next-developers-introduction): How the CMS + Next.js track is organized and what you need before you start - [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup): Set up your development environment for CMS integration with Next.js - [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries): Every CMS GraphQL query and mutation, with arguments and examples - [SSR Fetching](https://erxes.io/docs/content-plug-cms-next-developers-ssr): Fetch CMS data on the server in Next.js App Router pages and handle caching safely - [CSR Fetching](https://erxes.io/docs/content-plug-cms-next-developers-csr): Fetch CMS data from the browser without exposing administrative credentials - [CMS Migration](https://erxes.io/docs/content-plug-cms-next-developers-cms-migration): Move posts, categories, and pages from another CMS into the erxes CMS --- # Introduction How the CMS + Next.js track is organized and what you need before you start Build a Next.js website that displays erxes CMS content. The website examples use the Next.js App Router and native `fetch`. They are application examples; they are not shipped by the product repository. ## How content reaches the website The website's server calls the erxes gateway at its configured `/graphql` endpoint. The gateway authenticates the supplied credential and forwards the request context to the Content API. A server component can render that data directly; an interactive browser component can call a narrowly scoped endpoint on the website. The gateway's [hostname helper](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/utils.ts) considers forwarded hostname headers as well as the request hostname. Tenant database selection also depends on deployment configuration. Use the gateway URL and proxy configuration for your installation. ## Credentials and operation families A **Client Portal token** is sent as `x-app-token`. The gateway verifies the JWT and identifies the portal named by its `clientPortalId` claim. A separate `client-auth-token` can identify a signed-in portal user. See [gateway authentication](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts). An **Apps token**, sent as `erxes-app-token`, authenticates an application principal. Its access depends on the application's configuration and backend permission checks. Keep this administrative credential on the server. The CMS exposes two families: - `cms*` operations serve authenticated administration and tooling. - `cp*` operations serve client-portal integrations and generally derive their portal from request context ([`forClientPortal` resolver wrapper](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/wrapperResolvers.ts)). A prefix alone does not guarantee that an operation is read-only or returns only published content. For example, `cpPostList` accepts a status filter, and `cpPost` returns a matching record without adding a publication filter. The examples enforce publication status before returning data to visitors. See [post resolvers](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts) and [query builders](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/query-builders.ts). ## Keep the integration on the server Store credentials in server environment variables and keep modules that read them out of the browser import graph. Do not put them in `NEXT_PUBLIC_*` variables or a Next.js configuration `env` block. The [gateway's CORS configuration](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/main.ts) determines which browser origins it accepts. A separately hosted website may or may not be allowed, depending on that configuration. Server-to-server requests are not subject to browser CORS checks. The browser example uses a fixed post-search endpoint. Keeping a credential on the server is insufficient if an unrestricted proxy lets visitors execute any operation with it. ## Social publishing is a separate integration The CMS also has administrative sharing through Postiz. Reading published content through `cpPostList` does not enable social publishing. The [sharing service](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/service.ts) checks membership, CMS access, publication permissions, language access, and the selected post before queuing a delivery. This feature requires the corresponding `agent` service integration: the [bridge](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/bridge.ts) calls its `postizCms.execute` procedure. That service is not implemented in this repository. Confirm its availability in your deployment before enabling sharing; starting `content_api` alone is insufficient. See [Social publishing operations](https://erxes.io/docs/content-plug-cms-next-developers-queries#social-publishing-operations) for the CMS-side operations. These administrative operations do not belong in the website's public search endpoint. ## Follow the guides 1. [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup): configure the portal credential and server fetch helper. 2. [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries): check operation names, filters, pagination, and response shapes. 3. [Server-side Rendering](https://erxes.io/docs/content-plug-cms-next-developers-ssr): render published content and handle missing records. 4. [Client-side Rendering](https://erxes.io/docs/content-plug-cms-next-developers-csr): add interactive search through a fixed website endpoint. 5. [CMS Migration](https://erxes.io/docs/content-plug-cms-next-developers-cms-migration): update an older integration and verify its behavior. For product installation, see [Local Setup](https://erxes.io/docs/local-setup) or [Self-Hosting](https://erxes.io/docs/self-hosting). For the separate template repositories, see the [template developer track](https://erxes.io/docs/content-plug-template-developers-introduction). --- # Setup Set up your development environment for CMS integration with Next.js Connect a Next.js App Router website to erxes using a server-side Client Portal credential. Complete these steps before the [SSR](https://erxes.io/docs/content-plug-cms-next-developers-ssr) and [CSR](https://erxes.io/docs/content-plug-cms-next-developers-csr) examples. ## 1. Prepare the erxes instance Enable the **Content** plugin and create a Client Portal for the website using an account with the required administrative access. Confirm the website's CMS records belong to that portal. Interface labels vary by release; the backend stores the credential as [`ClientPortal.token`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/db/models/ClientPortal.ts). Use your deployment's gateway URL. A hosted installation commonly exposes `https://YOUR_TENANT.app.erxes.io/gateway/graphql`; the local setup guide uses `http://localhost:4000/graphql`. A self-hosted proxy may expose another prefix. Create a published test post with a title and slug. Also create a draft so you can verify that it is excluded from the website. ## 2. Obtain the portal token Copy the token from the intended Client Portal using your authorized administration interface. Send it as `x-app-token`. This is different from an Apps token sent as `erxes-app-token`. The [token manager](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/services/auth/jwtManager.ts) signs a JWT containing the portal ID. The [gateway](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts) verifies its signature and loads that portal. The `clientPortalChangeToken` implementation stores a newly signed token, but gateway verification does not compare incoming JWTs with that stored token. Do not assume changing the stored token invalidates previously issued JWTs. Coordinate credential invalidation with the deployment owner if a token was exposed. ## 3. Configure the website Add these variables to the website's private local environment file or hosting environment: ```dotenv ERXES_API_URL=https://YOUR_TENANT.app.erxes.io/gateway/graphql ERXES_APP_TOKEN=REPLACE_WITH_CLIENT_PORTAL_TOKEN ERXES_FILE_URL=https://YOUR_TENANT.app.erxes.io/gateway/read-file?key= ``` Here `ERXES_APP_TOKEN` is a website variable name for the **Client Portal token**; it is not the administrative Apps credential. Keep the private file out of version control and put empty placeholders in the project's example environment file. Do not add these credentials to `next.config`'s `env` object or prefix them with `NEXT_PUBLIC_`. Use `server-only` to detect accidental imports into a client component's dependency graph: ```bash npm install server-only ``` Use the equivalent command if your website uses another package manager. The examples use native `fetch`, so Apollo Client is optional. Existing Apollo applications can use the same GraphQL documents and credential boundaries. ## 4. Add the server fetch helper Create this module in the **website**, not in the erxes product repository: ```ts // lib/erxes/server.ts import "server-only"; type GraphQLResponse<T> = { data?: T | null; errors?: Array<{ message: string }>; }; export async function erxesQuery<T>( query: string, variables: Record<string, unknown> = {}, ): Promise<T> { const apiUrl = process.env.ERXES_API_URL; const token = process.env.ERXES_APP_TOKEN; if (!apiUrl || !token) { throw new Error("CMS server configuration is missing"); } const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", "x-app-token": token, }, body: JSON.stringify({ query, variables }), cache: "no-store", signal: AbortSignal.timeout(10000), }); if (!response.ok) { throw new Error(`CMS HTTP request failed (${response.status})`); } const result = (await response.json()) as GraphQLResponse<T>; if (result.errors?.length || result.data == null) { throw new Error("CMS returned a GraphQL error or no data"); } return result.data; } ``` The helper checks HTTP and GraphQL failures separately. It uses a fixed server-configured URL and credential, and disables caching. The generic type describes the expected response; it is not runtime schema validation. Do not expose this generic helper through an endpoint that accepts arbitrary client-supplied queries. The [CSR guide](https://erxes.io/docs/content-plug-cms-next-developers-csr) provides a fixed operation with limited inputs. ## 5. Build file URLs Attachment URLs may already be complete URLs or may be storage keys. A website helper can make this distinction: ```ts // lib/erxes/file-url.ts import "server-only"; export function erxesFileUrl(value?: string | null): string { if (!value) return ""; if (/^https?:\/\//i.test(value)) return value; if (value.startsWith("/")) return value; const base = process.env.ERXES_FILE_URL; if (!base) throw new Error("CMS file endpoint is missing"); const url = new URL(base); url.searchParams.set("key", value); url.searchParams.set("inline", "true"); return url.toString(); } ``` Confirm whether relative paths in your stored content belong to the website or the gateway; this helper preserves them. Pass the resulting URLs as props to browser components. The [file route](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/routes/fileRoutes.ts) accepts a file key and options such as `inline` and `width`. If you use `next/image`, configure its remote image patterns for the actual asset hosts and paths, which can include storage/CDN hosts beyond the gateway. ## 6. Check it works 1. Fetch the published test post with the [SSR example](https://erxes.io/docs/content-plug-cms-next-developers-ssr); it renders, and the draft does not appear. 2. Send an incorrect token; the request errors. 3. In the [CSR example](https://erxes.io/docs/content-plug-cms-next-developers-csr), inspect browser requests: they reach the website's `/api/posts` endpoint and carry no erxes credential. > [!WARNING] > **Empty response or browser CORS errors** > > An empty successful response usually means the records are not associated to the portal or not published; check that before changing the query. Browser CORS errors mean browser code is calling the gateway directly; the examples route through the website's own endpoint. For Next.js server/client boundaries, see the [official documentation](https://nextjs.org/docs/app/getting-started/server-and-client-components). --- # Queries Every CMS GraphQL query and mutation, with arguments and examples A reference for website and administrative GraphQL operations in the erxes CMS. Work through [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup) first. Execute these documents on the server with the credential appropriate to the operation. The browser example exposes only a fixed published-post search endpoint. ## Naming convention: `cp*` versus `cms*` The CMS exposes two parallel families of operations. CMS `cp*` operations serve client-portal integrations. Operations marked `forClientPortal: true` bypass the normal team-member permission wrapper; inspect each resolver's context and argument handling. `cpPostList`, for example, uses the Client Portal identified by `x-app-token`. See [`wrapperResolvers`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/wrapperResolvers.ts) and [`cpPostList`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts). `cms*` operations are for the erxes admin UI and authenticated tooling. They go through the permission wrapper, they require a logged-in team member or an Apps token, and most of them require an explicit `clientPortalId` argument: `cmsPages`, `cmsPageList`, `cmsMenus`, `cmsMenuList`, and `cmsMenu` all throw `clientPortalId is required` without one. See [`queries/page.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts#L8-L53) and [`queries/menu.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/menu.ts#L6-L60). The examples below are an API reference, not a browser operation allow-list. `cp*` does not imply published-only results or read-only behavior. Public post lists must enforce `status: published`; detail and page results need their own publication checks before exposure. Administrative `cms*` integrations can run on an authorized server without exposing their credentials to visitors. ## Pagination: two incompatible styles This is the single most common source of `Unknown argument` errors. Most list operations use the shared **cursor** arguments, defined once in [`apollo/constants.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/constants.ts#L1-L16). The shared cursor helper defaults to 20 items and rejects limits outside 1–100: - `limit: Int` - `cursor: String` - `cursorMode: CURSOR_MODE`: `inclusive` or `exclusive` - `direction: CURSOR_DIRECTION`: `forward` or `backward` - `orderBy: JSON` - `sortMode: String` - `aggregationPipeline: [JSON]` These are backend schema capabilities. Do not pass arbitrary browser-supplied aggregation pipelines, ordering objects, or filters through a public website endpoint. Fix the operation and validate the inputs that the website actually supports. `page` and `perPage` are **not** part of that set. They belong to the offset style, which in the CMS is used by exactly one operation: `cpPostListWithPagination`. Its arguments come from [`GQL_OFFSET_PARAM_DEFS`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/constants.ts#L11-L16): `page`, `perPage`, `sortField`, `sortDirection`. List operations that paginate return an envelope, not a bare array. `PostList`, `PageList`, `PostTagList`, and `PostCategoryListResponse` all carry `pageInfo`, whose shape is [`PageInfo`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/commonTypeDefs.ts#L35-L41): `hasNextPage`, `hasPreviousPage`, `startCursor`, `endCursor`. Feed `endCursor` back in as `cursor` to get the next page. Operations ending in a bare list (`cpPosts`, `cpPages`, `cpMenus`, `cpCmsMenuList`, `cpMostViewedPosts`, `cpCustomPostTypes`, `cpCustomFieldGroups`) return an array with no `totalCount`, even when the schema accepts the shared cursor arguments. The CMS list helper recognizes lowercase `sortDirection: "asc"`; other values select descending order. An explicit `orderBy` takes precedence over `sortField`. See the [pagination implementation](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mongo/mongoose-utils.ts) and [CMS base resolvers](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/base-resolvers.ts). ## Posts ### `cpPostList`: paginated post list The workhorse for blog indexes, category archives, and search results. ```graphql query CpPostList( $type: String $featured: Boolean $categoryIds: [String] $tagIds: [String] $searchValue: String $status: PostStatus $authorId: String $language: String $webId: String $dateField: PostDateField $dateFrom: Date $dateTo: Date $sortField: String $sortDirection: String $limit: Int $cursor: String ) { cpPostList( type: $type featured: $featured categoryIds: $categoryIds tagIds: $tagIds searchValue: $searchValue status: $status authorId: $authorId language: $language webId: $webId dateField: $dateField dateFrom: $dateFrom dateTo: $dateTo sortField: $sortField sortDirection: $sortDirection limit: $limit cursor: $cursor ) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } posts { _id title slug excerpt type status featured publishedDate createdAt updatedAt viewCount authorKind authorId categoryIds thumbnail { url name type size } customPostType { _id code label } tags { _id name slug colorCode } categories { _id name slug } } } } ``` Argument notes, all from [`schemas/posts.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L154-L195): - `categoryIds` is `[String]`, plural. There is no `categoryId` argument on any post operation. - `status` is the `PostStatus` enum: `draft`, `published`, `scheduled`, `archived`. Pass `"published"` for public listings. - `type` filters by post type; combine it with `cpCustomPostTypes` below to build a type-aware archive. - The schema accepts `dateField`, `dateFrom`, and `dateTo`. Verify date-filter behavior in your installed resolver before depending on it. - `webId` scopes results to one Web Builder site. Omit it for a standalone CMS. - `language` requests translated field values. The resolver falls back to the stored values when no translation exists. Omit `content` from list queries. It is the full HTML body and dominates payload size; fetch it only in the detail operation. ### `cpPost`: single post Resolves by `_id`, by `slug`, or by `identifier`. ```graphql query CpPost( $id: String $slug: String $identifier: String $count: Int $language: String ) { cpPost( _id: $id slug: $slug identifier: $identifier count: $count language: $language ) { _id title slug excerpt content type status featured publishedDate scheduledDate createdAt updatedAt viewCount authorKind authorId categoryIds seoTitle seoDescription videoUrl thumbnail { url name type size } images { url name type size } video { url name type size } documents { url name type size } attachments { url name type size } pdfAttachment { pdf { url name type } pages { url name type } } customFieldsData customFieldsMap translations { _id objectId language title excerpt } } } ``` `cpPost` increments the view counter when it finds a post. Repeated requests, prefetching, and caching therefore affect the count; it is not a count of unique visitors. See [`increaseViewCount`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts#L523-L549) and the [SSR caching explanation](https://erxes.io/docs/content-plug-cms-next-developers-ssr). The resolver does not filter publication status; request `status` and check it before exposing a record. The `clientPortalId` argument appears in the schema but the resolver ignores it and always uses the portal from the token. See [`cpPost`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts#L523-L549). There is no `cpPostDetail`. Older guides and migrated codebases refer to it; the operation is `cpPost`. ### `cpPosts`: list without a pagination envelope Same arguments as `cpPostList` but returns `[Post]` directly, with no `totalCount` and no `pageInfo`. Use it when you need a fixed, small set (the three latest posts in a sidebar, for example). ```graphql query CpPosts($type: String, $status: PostStatus, $limit: Int, $sortField: String, $sortDirection: String) { cpPosts( type: $type status: $status limit: $limit sortField: $sortField sortDirection: $sortDirection ) { _id title slug excerpt publishedDate thumbnail { url name type } } } ``` ### `cpPostListWithPagination`: offset pagination The only CMS operation that accepts `page` and `perPage`. It returns `PostListPagination`, which has `posts` and `totalCount` but **no `pageInfo`**. See [`schemas/posts.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L92-L95). ```graphql query CpPostListWithPagination( $page: Int $perPage: Int $sortField: String $sortDirection: String $status: PostStatus $categoryIds: [String] $type: String $language: String ) { cpPostListWithPagination( page: $page perPage: $perPage sortField: $sortField sortDirection: $sortDirection status: $status categoryIds: $categoryIds type: $type language: $language ) { totalCount posts { _id title slug excerpt publishedDate thumbnail { url name type } } } } ``` Prefer this over cursor pagination when you need numbered page links (`?page=3`). Prefer `cpPostList` when you need infinite scroll. ### `cpMostViewedPosts`: popularity list `days` is required and must be a positive integer. It is capped at the backend's 30-day view-retention window, and `limit` defaults to 10 and is clamped between 1 and 100. See [`findMostViewedPosts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts#L101-L203). ```graphql query CpMostViewedPosts($days: Int!, $limit: Int, $type: String, $language: String, $webId: String) { cpMostViewedPosts(days: $days, limit: $limit, type: $type, language: $language, webId: $webId) { _id title slug viewCount recentViewCount thumbnail { url name type } } } ``` `recentViewCount` is the count inside the requested window and is only present on this operation. ## Pages Pages carry the section list a template renders. `Page.pageItems` is the field that matters for composable layouts; each item has a `type`, an `order`, and a free-form `config` object. See [`schemas/page.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts#L3-L39). ### `cpPageList`: all pages ```graphql query CpPageList($language: String, $limit: Int, $cursor: String) { cpPageList(language: $language, limit: $limit, cursor: $cursor) { totalCount pageInfo { hasNextPage endCursor } pages { _id name slug description coverImage type status createdUserId createdAt updatedAt customFieldsData customFieldsMap } } } ``` `cpPages(language)` is the unpaginated variant returning `[Page]`. Use it to enumerate routes for `generateStaticParams`. ### `cpCmsPageDetail`: one page with its sections ```graphql query CpPage($slug: String, $id: String, $language: String) { cpCmsPageDetail(slug: $slug, _id: $id, language: $language) { _id name slug description coverImage type status content createdAt updatedAt customFieldsData customFieldsMap pageItems { _id name type content order objectType objectId config } } } ``` Pass either `slug` or `_id`; if you pass neither, the resolver returns `null` rather than an error. See [`cpCmsPageDetail`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts#L121-L138). The portal is applied from the token, so a slug from another portal cannot be read. `config` is a `JSON` scalar. The GraphQL schema does not describe section-specific fields inside it. Validate that content against the selected template's expected fields at the rendering boundary. `Page.createdUser` resolves to a `User` from the core subgraph. `createdUserId` is a nullable scalar field that can be selected without those user details. Choose the author information the website needs. ## Categories ### `cpCategories`: category list Returns `PostCategoryListResponse`, whose array field is named `list`, not `categories`, and not the objects themselves. See [`schemas/category.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/category.ts#L27-L31). ```graphql query CpCategories( $status: CategoryStatus $searchValue: String $language: String $sortField: String $sortDirection: String $limit: Int $cursor: String ) { cpCategories( status: $status searchValue: $searchValue language: $language sortField: $sortField sortDirection: $sortDirection limit: $limit cursor: $cursor ) { totalCount pageInfo { hasNextPage endCursor } list { _id name slug description parentId status createdAt updatedAt customFieldsData parent { _id name slug } } } } ``` `status` is the `CategoryStatus` enum: `active` or `inactive`. Pass `"active"` for public navigation. `cpCategories` honours an explicit `clientPortalId` argument, falling back to the token's portal when it is absent: `args.clientPortalId || clientPortal?._id`. See [`cpCategories`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/category.ts#L57-L70). For a website, keep portal selection on the server and do not accept a browser-supplied portal ID. ### `cpCmsCategoryDetail`: one category ```graphql query CpCategory($slug: String, $id: String, $language: String) { cpCmsCategoryDetail(slug: $slug, _id: $id, language: $language) { _id name slug description parentId status customFieldsData customFieldsMap } } ``` Pair it with `cpPostList(categoryIds: [$id], status: "published")` to render a category archive. ## Menus Menu items are returned hydrated, so `parent` is populated and the flat list can be assembled into a tree client-side. `kind` is a free-form string; the Web Builder deployer looks for `main` and `footer`. See [`deploy`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L166-L181). ### `cpMenus`: navigation for a site ```graphql query CpMenus($kind: String, $language: String, $webId: String) { cpMenus(kind: $kind, language: $language, webId: $webId) { _id parentId label contentType contentTypeId type linkType kind icon url order openInNewTab target linkedContent { _id title slug linkType } parent { _id label url order } } } ``` The field is `contentTypeId`, lower-case `d`. `contentTypeID` does not exist and is a frequent typo in migrated code. See [`MenuItem`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/menu.ts#L19-L38). `linkType` is the `MenuLinkType` enum (`URL`, `PAGE`, `POST`, `CATEGORY`, `TAG`) and is what you should branch on when building a link. `linkedContent` gives you the target's `_id`, `title`, and `slug` so you can build the href without a second round trip. `cpCmsMenuList(clientPortalId, kind, language, ...)` accepts the shared cursor arguments but still returns a bare `[MenuItem]` array; there is no `MenuItemResponse` envelope in the result. Unlike `cpMenus` it ignores `webId`, and its `clientPortalId` argument is also ignored: the resolver always uses the portal from the token. See [`cpCmsMenuList`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/menu.ts#L87-L104). ## Tags and custom post types ### `cpCmsTags` Returns `PostTagList`; the array field is `tags`. ```graphql query CpTags($searchValue: String, $language: String, $sortField: String, $sortDirection: String, $limit: Int) { cpCmsTags( searchValue: $searchValue language: $language sortField: $sortField sortDirection: $sortDirection limit: $limit ) { totalCount pageInfo { hasNextPage endCursor } tags { _id name slug colorCode } } } ``` ### `cpCustomPostTypes` Use this to discover the post `type` values your instance has, then feed one into `cpPostList(type:)`. ```graphql query CpCustomPostTypes($searchValue: String) { cpCustomPostTypes(searchValue: $searchValue) { _id code label pluralLabel description createdAt } } ``` `cpCustomFieldGroups(searchValue, pageId, categoryId, postType, postId)` returns the field-group definitions behind `customFieldsData` and `customFieldsMap`. See [`schemas/customPostType.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/customPostType.ts#L78-L84). ## Post reactions The schema includes reaction and view-counter mutations. The `cp*` family also contains other writes, so its prefix must not be used as a read-only access rule. ```graphql mutation CpPostsReact($id: String!, $reaction: PostReactionType!, $action: ReactionModifyType!) { cpPostsReact(_id: $id, reaction: $reaction, action: $action) { _id reactions reactionCounts } } ``` `PostReactionType` is `like | love | angry | sad | happy`; `ReactionModifyType` is `inc | dec`. `reactionCounts` is a `JSON` map of reaction to count. `cpPostsIncrementViewCount(_id: String!)` is the explicit counter bump, which you only need if you deliberately avoid `cpPost`. See [`schemas/posts.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L197-L214). Implement reactions through a separately authorized endpoint with input validation and appropriate request limits. The post-search endpoint in the CSR guide does not accept mutations. ## The `author` field `Post.author` is declared as `union Author = User`, and `User` comes from the core subgraph, not from `content_api`. See [`schemas/posts.ts:39`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L39). Two consequences: - There is no `ClientPortalUser` type in the union. A fragment `... on ClientPortalUser` is a schema validation error. The Client Portal user type in core is `CPUser`, and it is not a member of `Author`. - Selecting subfields of `author` requires the federated schema to include `User`. Introspect your own gateway before relying on it: ```bash set -a; source .env.local; set +a curl -sS "$ERXES_API_URL" \ -H "content-type: application/json" \ -H "x-app-token: $ERXES_APP_TOKEN" \ -d '{"query":"query { __type(name: \"Author\") { kind possibleTypes { name fields { name } } } }"}' ``` `authorKind` and `authorId` can be selected without fetching the federated user's fields. Decide which author information the website should expose; an identifier alone is not a human-readable byline. ## Admin-side operations Listed for completeness. These need a team-member session or an Apps token and must stay server-side. ### `cmsPostList` ```graphql query CmsPostList( $clientPortalId: String $status: PostStatus $categoryIds: [String] $searchValue: String $type: String $language: String $limit: Int $cursor: String ) { cmsPostList( clientPortalId: $clientPortalId status: $status categoryIds: $categoryIds searchValue: $searchValue type: $type language: $language limit: $limit cursor: $cursor ) { totalCount pageInfo { hasNextPage endCursor } posts { _id title slug status type authorKind authorId createdAt updatedAt scheduledDate publishedDate } } } ``` Both `cmsPostList` and `cpPostList` can return unpublished records when no publication filter is applied. Keep preview routes authenticated and enforce the intended publication policy on public endpoints. ### `cmsPages` and `cmsPage` ```graphql query CmsPages($clientPortalId: String, $searchValue: String, $language: String, $limit: Int) { cmsPages(clientPortalId: $clientPortalId, searchValue: $searchValue, language: $language, limit: $limit) { _id name slug description type status createdUserId createdAt updatedAt } } ``` ```graphql query CmsPage($id: String, $slug: String, $clientPortalId: String, $language: String) { cmsPage(_id: $id, slug: $slug, clientPortalId: $clientPortalId, language: $language) { _id name slug description status content pageItems { _id name type order objectType objectId config } } } ``` `cmsPage` returns `null` when neither `_id` nor `slug` is supplied. ### `cmsCategories` ```graphql query CmsCategories($clientPortalId: String, $status: CategoryStatus, $searchValue: String, $language: String, $limit: Int) { cmsCategories( clientPortalId: $clientPortalId status: $status searchValue: $searchValue language: $language limit: $limit ) { totalCount pageInfo { hasNextPage endCursor } list { _id name slug parentId status } } } ``` ### `cmsMenuList` ```graphql query CmsMenuList($clientPortalId: String, $kind: String, $language: String) { cmsMenuList(clientPortalId: $clientPortalId, kind: $kind, language: $language) { _id parentId label contentType contentTypeId linkType kind icon url order openInNewTab target } } ``` `clientPortalId` is required at runtime even though the schema marks it nullable. ### `cmsTags` and `contentCMSList` ```graphql query CmsTags($clientPortalId: String, $searchValue: String, $language: String, $limit: Int) { cmsTags(clientPortalId: $clientPortalId, searchValue: $searchValue, language: $language, limit: $limit) { totalCount tags { _id name slug colorCode } } } ``` ```graphql query ContentCmsList { contentCMSList { _id name description clientPortalId domain publicUrl language languages postUrlField postUrlPrefix accessPolicy allowComments defaultPostStatus } } ``` `contentCMSList` is how you discover the `clientPortalId` values a team member can reach. `accessPolicy` is `open` or `assigned`; with `assigned`, only the members in `assignedMemberIds` plus owners can use that CMS. See [`cms-access.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/cms-access.ts#L1-L52). ## Execute a document on the server Copy the operation you need into a server module and call the `erxesQuery` helper from [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup): ```ts import "server-only"; import { erxesQuery } from "@/lib/erxes/server"; const query = ` query WebsitePostCount { cpPostList(status: published, limit: 1) { totalCount } } `; export async function getPublishedPostCount() { const data = await erxesQuery<{ cpPostList: { totalCount: number | null } | null; }>(query); return data.cpPostList?.totalCount ?? 0; } ``` These documents also work with an appropriately configured GraphQL client. Select only the fields the website renders. Keep server-side operations and credential handling separate from the browser's allowed inputs. ## Social publishing operations The [Postiz GraphQL module](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/graphql.ts) adds CMS-side operations for a separately configured social-publishing integration: - `cmsPostizOptions(clientPortalId: String!, language: String)` returns `enabled`, `canManage`, and channels with `id`, `name`, `provider`, and `usable`. - `cmsPostizDeliveries(postId: String!, language: String!)` returns up to 50 recent deliveries for the selected post and language. - `cmsPostizValidate(input: CmsPostizShareInput!)` validates a share request before queuing it. - `cmsPostizShare(input: CmsPostizShareInput!)` queues deliveries and returns their records. - `cmsPostizRetry(id: String!, reviewedInPostiz: Boolean!)` requests a retry through the service's state checks. - `cmsPostizEnable(clientPortalId: String!, language: String!, enabled: Boolean!)` changes sharing enablement through the external integration. For an authenticated administration screen, inspect available channels before offering a share action: ```graphql query CmsSocialPublishingOptions($clientPortalId: String!, $language: String) { cmsPostizOptions(clientPortalId: $clientPortalId, language: $language) { enabled canManage channels { id name provider usable } } } ``` The [input validator](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/service.ts) requires `postId`, a UUID `requestId`, `language`, one to ten `channelIds`, a nonempty caption of at most 2,800 characters, and a `media` array of up to four URLs. Selected media must already belong to the post's thumbnail or images. The backend appends the public article URL to the caption and validates each selected channel through the bridge. Only ordinary posts with `status: published` are shareable. The [URL builder](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/content.ts) uses the CMS public URL or domain, its post URL prefix, and its configured `_id`, `slug`, or `count` identifier. Configure these to match your website before sharing; a valid CMS record alone does not establish a working public article URL. These operations require administrative access and a running `agent` service providing `postizCms.execute`. This repository has the Content-side bridge, not that service implementation. A queued delivery is not proof of publication; inspect its delivery state and returned URL. Do not expose these operations through the public website endpoint. ## Troubleshooting - **HTTP 401/403 or "forbidden" GraphQL errors**: the credential is missing, invalid, or the wrong family. Public content reads need the Client Portal token in `x-app-token`; `cms*` operations need a team-member session or an Apps token in `erxes-app-token`. - **A successful query returns an empty list**: the token matched a portal, but the records belong to a different `clientPortalId`. Confirm the portal association of your posts, pages, and menus; do not fix it by passing a browser-supplied `clientPortalId`. - **`Unknown argument "page"` / `"perPage"`**: only `cpPostListWithPagination` uses offset pagination. Every other list takes `limit`/`cursor`. - **`Cannot return null for non-nullable` on `clientPortalId`**: some CMS records require a portal association; audit the data rather than removing the field from the selection set. --- # SSR Fetching Fetch CMS data on the server in Next.js App Router pages and handle caching safely Render CMS content on the server so the initial HTML contains the page's content. This example uses the server helper from [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup) and the post operations in [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries). ## Fetch published posts Create a reusable server module. The publication filter is fixed in the document, so callers cannot replace it with `draft`. ```ts // lib/erxes/posts.ts import "server-only"; import { cache } from "react"; import { erxesQuery } from "./server"; export type PostSummary = { _id: string; title: string | null; slug: string | null; excerpt: string | null; status: string | null; }; type PostListData = { cpPostList: { posts: Array<PostSummary | null> | null; } | null; }; const POST_LIST_QUERY = ` query WebsitePublishedPosts($searchValue: String) { cpPostList(status: published, limit: 12, searchValue: $searchValue) { posts { _id title slug excerpt status } } } `; export async function getPublishedPosts(searchValue = "") { const data = await erxesQuery<PostListData>(POST_LIST_QUERY, { searchValue }); return (data.cpPostList?.posts ?? []).filter( (post): post is PostSummary => post !== null && post.status === "published" && Boolean(post.slug), ); } const POST_QUERY = ` query WebsitePost($slug: String!) { cpPost(slug: $slug) { _id title slug excerpt status } } `; export const getPublishedPost = cache(async (slug: string) => { const data = await erxesQuery<{ cpPost: PostSummary | null }>( POST_QUERY, { slug }, ); const post = data.cpPost; return post?.status === "published" ? post : null; }); ``` The [post query builder](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/query-builders.ts) only adds a status filter when one is supplied. The [detail resolver](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts) does not itself restrict the result to published posts. Check the status before returning a detail record or using its fields in metadata. ## Render the index ```tsx // app/blog/page.tsx import { getPublishedPosts } from "@/lib/erxes/posts"; export default async function BlogPage() { const posts = await getPublishedPosts(); return ( <main> <h1>Blog</h1> {posts.length === 0 ? <p>No published posts yet.</p> : ( <ul> {posts.map(post => ( <li key={post._id}> <a href={`/blog/${encodeURIComponent(post.slug!)}`}> {post.title || "Untitled post"} </a> {post.excerpt ? <p>{post.excerpt}</p> : null} </li> ))} </ul> )} </main> ); } ``` This example displays the first 12 matching posts. For a complete archive, add cursor pagination using the `pageInfo` fields documented in [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries). ## Render a detail route This route-prop type follows the asynchronous `params` API in [Next.js 15 and later](https://nextjs.org/docs/app/api-reference/file-conventions/page#params-optional), including the product's Next.js 16 portal and help-center apps. If your separate website uses Next.js 14, use `params: { slug: string }` instead; the `await params` line can remain. ```tsx // app/blog/[slug]/page.tsx import { notFound } from "next/navigation"; import { getPublishedPost } from "@/lib/erxes/posts"; type Props = { params: Promise<{ slug: string }>; }; export default async function BlogPostPage({ params }: Props) { const { slug } = await params; const post = await getPublishedPost(slug); if (!post) notFound(); return ( <article> <h1>{post.title || "Untitled post"}</h1> {post.excerpt ? <p>{post.excerpt}</p> : null} </article> ); } ``` This minimal example displays plain text. To render the full `content` field, determine its stored format and use an appropriate renderer. If it contains HTML, sanitize it with a maintained server-side HTML sanitizer before inserting it as markup. Do not insert untrusted content directly with `dangerouslySetInnerHTML`. For SEO metadata, call the same `getPublishedPost` helper from `generateMetadata`; return missing-page metadata when it returns `null`. ## Caching and view counts The Setup helper uses `cache: "no-store"`. Adding `export const revalidate = 300` to a page does not turn those uncached requests into five-minute cached fetches. Choose caching at the data layer when the content and authorization model allow it. See [Next.js 14 data fetching and caching](https://nextjs.org/docs/14/app/building-your-application/data-fetching/fetching-caching-and-revalidating). React's `cache` wrapper deduplicates the detail helper within a server rendering request; it is not persistent storage across requests. `cpPost` increments the post view count, so repeated reads can increase that count while caching can reduce counted reads. It is not a reliable count of unique human visitors. For draft previews or user-specific content, keep caching and authorization separate from the public published-content path. ## Loading and errors Add an `app/blog/loading.tsx` loading UI and an `app/blog/error.tsx` error boundary with a useful retry action. A missing or unpublished record should use the not-found path; a failed upstream request should remain an error. Verify a published post, a draft, an unknown slug, an empty list, and an unavailable gateway. Continue with [Client-side Rendering](https://erxes.io/docs/content-plug-cms-next-developers-csr) for interactive search. ## Troubleshooting - **`data.cpPostList` is null or an HTTP error**: the `x-app-token` is missing or invalid. Confirm the token and gateway URL in the server environment. - **Empty `posts` array**: the portal token matched, but the portal has no published posts; check the records' `clientPortalId` association and `status`. - **Unknown argument errors**: compare your document with the signatures in [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries); most lists use `limit`/`cursor`, not `page`/`perPage`. --- # CSR Fetching Fetch CMS data from the browser without exposing administrative credentials Use a browser component for interactive search while keeping erxes credentials and publication rules on the website's server. Complete [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup) and add the `posts.ts` helper from [SSR Fetching](https://erxes.io/docs/content-plug-cms-next-developers-ssr) first. ## Create a fixed post-search endpoint The browser supplies only a search string. The server controls the GraphQL document, portal credential, publication filter, returned fields, and result limit. ```ts // app/api/posts/route.ts import { NextResponse } from "next/server"; import { getPublishedPosts } from "@/lib/erxes/posts"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET(request: Request) { const params = new URL(request.url).searchParams; const query = params.get("query") ?? ""; if ( query.length > 100 || params.getAll("query").length > 1 || [...params.keys()].some(key => key !== "query") ) { return NextResponse.json({ error: "Invalid search" }, { status: 400 }); } try { const posts = await getPublishedPosts(query.trim()); return NextResponse.json( { posts }, { headers: { "Cache-Control": "no-store" } }, ); } catch { return NextResponse.json( { error: "Content is temporarily unavailable" }, { status: 502 }, ); } } ``` This endpoint exposes only the published-post query. It does not accept an upstream URL, raw GraphQL, a portal ID, or a status override. A generic GraphQL forwarding endpoint would let visitors use the server credential for operations the website did not intend to expose. Apply the website's normal request limits when deploying public search. The erxes upstream timeout is set in the Setup helper. ## Add the search component The component waits briefly after typing, aborts superseded requests, and shows loading, empty, and error states. ```tsx // app/blog/_components/post-search.tsx "use client"; import { useEffect, useState } from "react"; import type { PostSummary } from "@/lib/erxes/posts"; export function PostSearch() { const [query, setQuery] = useState(""); const [posts, setPosts] = useState<PostSummary[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [attempt, setAttempt] = useState(0); useEffect(() => { const controller = new AbortController(); let active = true; setLoading(true); setError(""); setPosts([]); const timer = setTimeout(async () => { try { const response = await fetch( `/api/posts?query=${encodeURIComponent(query)}`, { signal: controller.signal }, ); if (!response.ok) throw new Error("Search failed"); const data: { posts: PostSummary[] } = await response.json(); if (active) setPosts(data.posts); } catch { if (active) setError("Search is unavailable. Please try again."); } finally { if (active) setLoading(false); } }, 300); return () => { active = false; clearTimeout(timer); controller.abort(); }; }, [query, attempt]); return ( <section aria-label="Search posts"> <label htmlFor="post-search">Search published posts</label> <input id="post-search" type="search" maxLength={100} value={query} onChange={event => setQuery(event.target.value)} /> <div aria-live="polite" aria-busy={loading}> {loading ? <p>Searching…</p> : null} {error ? <p role="alert">{error}</p> : null} {!loading && !error && posts.length === 0 ? <p>No posts found.</p> : null} </div> {error && !loading ? ( <button type="button" onClick={() => setAttempt(value => value + 1)}> Retry search </button> ) : null} <ul> {posts.map(post => ( <li key={post._id}> {post.slug ? ( <a href={`/blog/${encodeURIComponent(post.slug)}`}> {post.title || "Untitled post"} </a> ) : null} </li> ))} </ul> </section> ); } ``` The `import type` is erased from browser output; it does not import the server helper at runtime. For a larger application, place shared response types in a dedicated module containing no server code. Render `<PostSearch />` from the relevant page. The browser requests its own origin's `/api/posts`; no erxes credential is included. ## Existing Apollo applications You can retain Apollo Client for an existing application, but a browser-accessible GraphQL endpoint must enforce its allowed operations and validate inputs. Forwarding arbitrary queries with a hidden token does not provide that restriction. Pin the Apollo version used by the website and follow its matching React integration documentation. ## Signed-in users and mutations Reactions, account data, and form submissions need their own authorized endpoints and input validation. A portal token identifies the website; it does not identify a signed-in person. The [gateway middleware](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts) handles portal-user authentication separately. The `cp*` prefix is not a read-only guarantee. Do not add mutation forwarding to the search endpoint. ## Check it works Test normal search, no matches, rapid typing, a query over 100 characters, an unknown parameter, and an unavailable gateway. Confirm drafts never appear and browser requests contain no portal or Apps token. Review [CMS Migration](https://erxes.io/docs/content-plug-cms-next-developers-cms-migration) when replacing an older direct-to-gateway browser integration. ## Troubleshooting - **502 from `/api/posts`**: the upstream erxes call failed; check the gateway URL, token validity, and the 10-second timeout in the Setup helper. - **400 on a valid-looking search**: the endpoint rejects duplicate or unknown parameters and queries over 100 characters by design. - **Every search returns empty**: the portal identified by the token has no matching published posts; check `clientPortalId` on the records and `status`. - **A credential visible in browser requests**: the browser should only call its own origin. Audit for a `NEXT_PUBLIC_*` variable or a `next.config` `env` block exposing the token. --- # CMS Migration Move posts, categories, and pages from another CMS into the erxes CMS Update an older website integration to the current erxes API. This is an application-integration migration, not a database migration or a guarantee that every older deployment exposes the same schema. ## 1. Inventory the current integration Record the deployed erxes revision, website dependency versions, gateway URL, credential headers, GraphQL operations, and CMS/portal associations. Identify which requests run in the browser and which already run on the server. Distinguish public website reads from authenticated administration, previews, portal-user actions, and mutations. They may require different credentials and authorization. ## 2. Move credentials into server modules Follow [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup). Remove credential values from `next.config.env`, `NEXT_PUBLIC_*` variables, static configuration files, and client components. Rebuild the website to remove those values from newly generated browser assets. The public examples use a Client Portal token through `x-app-token`. Existing administrative integrations may continue to require an Apps token through `erxes-app-token`; keep those behind their own authorized server interface. If an Apps credential was published in browser assets, treat it as exposed and coordinate revocation and replacement through the application's administration. The [portal JWT path](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/services/auth/jwtManager.ts) verifies the signature and portal existence but does not compare the submitted JWT with the portal's stored token ([`ClientPortal.token`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/db/models/ClientPortal.ts)). Changing `ClientPortal.token` alone is therefore not proof that older JWTs are invalid. ## 3. Update operation names and shapes Compare each operation with [Queries](https://erxes.io/docs/content-plug-cms-next-developers-queries) and the schema for your installed version. Common corrections: - Use `categoryIds: [String]` for post filters, rather than `categoryId`. - Most list operations use `limit` and `cursor`. The dedicated `cpPostListWithPagination` operation accepts `page` and `perPage`. - A post list envelope contains `posts`; a page list contains `pages`; a category list contains `list`; a tag list contains `tags`. - The post detail field is `cpPost`; there is no `cpPostDetail`. - The page detail field is `cpCmsPageDetail`. - A menu item's field is `contentTypeId`, with a lowercase final `d`. - The `Author` union contains `User`; do not add an unsupported `ClientPortalUser` fragment. The [schema directory](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas) defines these operations and fields. A nullable schema argument may still be required by a resolver, so inspect runtime behavior as well. ## 4. Enforce publication policy Switching an operation from `cms*` to `cp*` does not automatically hide draft content. Post lists apply the requested status, while post detail reads do not add a published-only filter. Use a fixed `status: published` filter for public post lists. Verify the returned status before exposing a detail record or using its fields in metadata. Check pages, menus, categories, and other content families individually; do not infer their policy from the post API. The [SSR guide](https://erxes.io/docs/content-plug-cms-next-developers-ssr) demonstrates published-only list and detail handling. The [CSR guide](https://erxes.io/docs/content-plug-cms-next-developers-csr) exposes a fixed website search endpoint with bounded inputs. ## 5. Rendering and caching Keep server credential modules outside the browser dependency graph. A component's imports become part of the client graph when reached through a client boundary, even if each imported file does not declare `"use client"`. The Setup helper uses `cache: "no-store"`. Decide on a deliberate caching policy before adding revalidation settings; a page-level revalidation declaration does not override an explicitly uncached fetch. The `cpPost` detail resolver increments a view counter. Repeated metadata and page queries can increase it, and cached results can reduce counted reads. Verify analytics expectations separately from content rendering. Render stored content according to its format. Sanitize untrusted HTML before using an HTML insertion API, and preserve the application's image and link handling. ## 6. Test before switching traffic Test these cases against a staging instance with known records: - Published and draft posts, including direct detail URLs and metadata. - Unknown slugs and an empty portal. - Cursor boundaries, filters, and expected response envelopes. - Missing/invalid credentials and unavailable upstream services. - Browser requests and built assets without erxes credentials. - Rejected unsupported parameters and operations. - Authenticated previews or mutations through their separate authorized flows. - Image URLs and rich content rendering. If introspection is enabled in the development instance, compare the live schema with the query documents. Otherwise use the installed source schema or an administrator-provided export. A production deployment may intentionally disable introspection. Keep the previous website build and configuration available during rollout. Restoring a website build does not reverse product database migrations; handle a product upgrade according to its own migration procedure. ## Troubleshooting - **401/403 or GraphQL auth errors after migration**: the wrong credential family is being sent; the website needs the Client Portal token in `x-app-token`, not an Apps token, and not a `client-auth-token`. - **Valid token, empty lists**: the portal matched but has no matching records; re-check `clientPortalId` on posts, pages, and menus after a portal change. - **`Unknown argument` errors**: `page`/`perPage` only exist on `cpPostListWithPagination`; other lists take `limit`/`cursor`. Check the [Queries reference](https://erxes.io/docs/content-plug-cms-next-developers-queries). - **Drafts visible on public routes**: a `cp*` prefix does not filter publication status; enforce `status: published` on lists and check `status` on detail reads. - **Stale content after deployment**: the Setup helper uses `cache: "no-store"`; if you added caching, re-check `revalidate` settings and any CDN layer. --- # Client Portal Standalone portal backed by portal tokens Build a customer-facing site on top of an erxes Client Portal using the standalone Next.js template. See [Authentication & Permissions](https://erxes.io/docs/api-authentication) for the credential mechanics behind these headers. For a production portal implementation, compare [Help Center](https://erxes.io/docs/app-help-center), which applies the same pattern. A Client Portal is a configuration record ([`ClientPortals` collection](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/db/models/ClientPortal.ts)) in the `clientportal` module in Core API. It carries a portal token plus per-portal settings: authentication options (OTP, verification, token lifetimes, delivery method), styles, and plugin-specific config such as the CMS `clientPortalId` that scopes public content. Plugins expose `cp*` GraphQL operations (`cpPosts`, `cpGetTickets`, `cpForms`, `clientPortalCurrentUser`, …) that look up the portal from the request context instead of from a logged-in team member. That makes the portal token the only credential a public website needs. ## Two credentials, not one | Credential | What it is | Transport | Becomes | | --- | --- | --- | --- | | Portal token | JWT `{ clientPortalId }` signed with `JWT_TOKEN_SECRET`, issued by `clientPortalAdd` / `clientPortalChangeToken` | `x-app-token` header | `req.clientPortal` at the gateway, forwarded as the base64 `clientportal` header | | Portal-user session | JWT `{ userId, clientPortalId }` issued on login/verify/reset; separate refresh token (`type: "refresh"`) | `client-auth-token` cookie (httpOnly) or header when the portal's `deliveryMethod` is `header` | `req.cpUser`, forwarded as the base64 `cpuser` header | The gateway only identifies a portal user **after** the `x-app-token` has identified a valid portal, and the user must belong to that portal. Default lifetimes are 1 day (access) and 7 days (refresh), overridable per portal via `auth.authConfig`. `clientPortalUserRefreshToken` exchanges a refresh token for a new access token. Portal-user auth flows (register, verify, credentials or OTP login, forgot/reset password, change email/phone) are implemented in `backend/core-api/src/modules/clientportal/services/` and documented in [`docs/auth-flows.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/docs/auth-flows.md) inside that module. The mutations are the `clientPortalUser*` family plus `clientPortalCurrentUser`. ## The template app `apps/client-portal-template` is a standalone Next.js 16.0.3 (React 19) starter, not an Nx project, with its own `package-lock.json`. Apollo Client 4 plus `@apollo/client-integration-nextjs` provide both an RSC client and a browser client. ```bash cd apps/client-portal-template npm install npm run dev # next dev --port 3800 ``` Two Apollo entry points exist: - [`modules/apollo/apolloClient.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/client-portal-template/modules/apollo/apolloClient.ts): server components. Sends `x-app-token: NEXT_PUBLIC_ERXES_CP_TOKEN` to `${ERXES_API_URL}/graphql` with `credentials: 'include'` (needed for the `client-auth-token` cookie round trip). - [`modules/apollo/ApolloWrapper.tsx`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/apps/client-portal-template/modules/apollo/ApolloWrapper.tsx): client components (`"use client"`). Same token header against `${NEXT_PUBLIC_ERXES_API_URL}/graphql`. `modules/cpposts/` is a worked example: `getExamplePosts.ts` calls the `getCPExamplePosts` query exposed by the clientportal module, and `CPPosts.tsx` renders it. ### Environment variables | Variable | Used by | Purpose | | --- | --- | --- | | `ERXES_API_URL` | server Apollo client | Gateway base URL, no trailing slash (e.g. `http://localhost:4000`) | | `NEXT_PUBLIC_ERXES_API_URL` | browser Apollo client | Same gateway, reachable from the browser | | `NEXT_PUBLIC_ERXES_CP_TOKEN` | both clients | Portal token sent as `x-app-token` | > [!WARNING] > **The template leaks the portal token to the browser** > > `NEXT_PUBLIC_*` variables end up in the client bundle, so the template sends `x-app-token` from the browser. The gateway CORS list must allow the portal's origin. For a hardened deployment move the token to a non-public variable read only by the RSC client (`apolloClient.ts`), or proxy all GraphQL through your own route handler. Never use an Apps token (`erxes-app-token`) in a public app. There is no `.env.example` or Dockerfile in the template; copy `apps/help-center`'s `docker-entrypoint.sh` pattern if you want one image to serve multiple gateways. ## Walkthrough ### 1. Create the portal and copy its token In erxes, create a Client Portal (Settings → Client portal, or via `clientPortalAdd`). The creation call signs and stores the portal token; rotate it later with `clientPortalChangeToken`. If you use the CMS or Frontline plugins, link them to this portal in their own settings. ### 2. Point the template at your gateway ```dotenv ERXES_API_URL=http://localhost:4000 NEXT_PUBLIC_ERXES_API_URL=http://localhost:4000 NEXT_PUBLIC_ERXES_CP_TOKEN=<portal token> ``` Restart `npm run dev`; `loadEnvConfig` in `apolloClient.ts` picks up `.env.local`. ### 3. Exercise a query and a session `getCPExamplePosts` renders on the home page. Then exercise a session: call `clientPortalUserRegister` + `clientPortalUserVerify` (or `clientPortalUserLoginWithCredentials`) and confirm the `client-auth-token` cookie is set, then `clientPortalCurrentUser` returns the visitor. ## Check it works 1. With only `x-app-token` set, a `cp*` query works but `clientPortalCurrentUser` returns null; the portal is known, the visitor is anonymous. 2. After login, the same `client-auth-token` cookie works from the browser and from curl (`-H "client-auth-token: <jwt>"`). 3. A token from a different portal sees only its own data; the gateway binds `cpUser` to `clientPortalId`. --- # Introduction Understand Company Brain and choose between OpenClaw, Hermes, and Postiz. Welcome to the **Company Brain Plugin** — the layer of erxes where your workspace gains AI assistants and a social media pipeline, side by side with the Content Plugin you may already use. Company Brain ships three modules from a single plugin: - **OpenClaw Assistant** — a dedicated AI assistant with its own isolated runtime, connected to the model provider of your choice. - **Hermes Agent** — a second, fully isolated agent runtime with persistent storage, erxes workspace tools, and a shared skill registry. - **Social / Postiz** — a connected social media workspace where your team drafts, reviews, schedules, and publishes content. ## What is Company Brain? Company Brain is the umbrella name for the plugin's surfaces inside erxes. It is where a workspace creates dedicated AI assistants, connects a model provider, shares assistant access with team members, and connects assistants to Discord. Postiz adds a social publishing surface governed by its own erxes access model. Company Brain is not a single chatbot bolted onto erxes. Each assistant gets its own isolated runtime that erxes provisions for you, and Postiz gets its own workspace provisioned through the Postiz gateway. Access, billing, and runtime isolation are all handled by the plugin. ## What are the three modules for? ### OpenClaw Assistant OpenClaw is a general-purpose assistant runtime for day-to-day company work: answering questions with your providers, running tools, and optional Discord presence. - Create multiple assistants, each with its own provider and model. - Connect with a provider API key or a provider subscription plan. - Invite team members to an assistant and open the runtime from the browser. - Connect the assistant to a Discord server and channel. - Transfer an assistant between erxes SaaS organizations without losing its runtime or its provider connection. ### Hermes Agent Hermes is a second assistant runtime kind, separate from OpenClaw. Choose Hermes when you want: - A per-assistant isolated namespace with persistent storage — credentials and workspace state survive restarts. - An embedded WebUI reached through a short-lived, backend-minted login URL (never a raw password in the browser). - Optional erxes workspace tools: after an OAuth approval, the assistant can search and call only the erxes capabilities your permissions allow. - The shared skill pot: publish community skills for your organization or use verified skills promoted by erxes. - Discord binding with the same official erxes AI Assistant bot as OpenClaw, with assistant- and runtime-scoped channel bindings. ### Social / Postiz Postiz is the social media module. It is provisioned differently: instead of a runtime per assistant, your tenant gets a workspace on a shared, managed Postiz installation. - The erxes owner connects the workspace and selects the initial Postiz Admin from your active erxes users. - Admins invite teammates as Postiz Admins or Members. - Members draft, review, and schedule social content in Postiz. - Optionally, the erxes CMS can hand approved content to Postiz through a signed server-to-server bridge (disabled by default). ## What Company Brain is not - It is not a single sign-on service. Postiz accounts and Hermes/OpenClaw runtime logins are separate from erxes authentication. - It is not automatic access to every erxes module. An assistant can only act through explicitly approved tools and permissions. - It is not an unmanaged BYO-key chat. Runtimes are provisioned and monitored by the plugin; providers are connected through managed flows. ## Who this guide is for - **Workspace owners** creating assistants, connecting providers, selecting the initial Postiz Admin, and managing access. - **Team members** who are invited to an assistant or the social workspace. - **Developers and operators** deploying the plugin on erxes SaaS or an enterprise installation, including the Kubernetes topology. ## How to read these docs Choose the section for your role in the sidebar. Introduction stays outside these sections so everyone can start with the same overview. ### For Workspace Owners and Teams Start with [Getting started](https://erxes.io/docs/company-brain-getting-started), then follow the guide for your module: - [OpenClaw Assistant](https://erxes.io/docs/company-brain-openclaw) — create an assistant, connect a provider, invite members, and connect Discord. - [Hermes Agent](https://erxes.io/docs/company-brain-hermes) — use the isolated runtime, connect workspace tools, and manage the assistant. - [Social / Postiz](https://erxes.io/docs/company-brain-postiz) — set up your workspace, manage team access, and prepare social content. ### For Developers Read [How it works](https://erxes.io/docs/company-brain-architecture) for plugin boundaries, provisioning flows, tenant isolation, and the CMS integration. For a local environment, continue to [Setup and Kubernetes](https://erxes.io/docs/company-brain-setup). ### For Administrators and Operators Use [Setup and Kubernetes](https://erxes.io/docs/company-brain-setup) for prerequisites, configuration, deployment order, and smoke checks. Keep [Access and troubleshooting](https://erxes.io/docs/company-brain-troubleshooting) nearby for provisioning, provider, quota, and workspace access issues. --- # Getting started Set up your first assistant or social workspace and invite your team. This page walks you through your first hours with Company Brain: creating an assistant, inviting your team, and opening the social workspace. ## Prerequisites - An erxes workspace where you are the **organization owner** (assistant and workspace provisioning are owner-level actions). - A Company Brain entitlement. Business School plans of 3, 10, and 25 people grant 3, 10, and 25 total assistant slots shared by OpenClaw and Hermes; existing gifts and add-ons also cover both kinds. Enterprise installations are unlimited. - For assistants: a model provider account — either an API key or an active subscription plan with one of the supported providers. - For Postiz on erxes SaaS: access must be enabled for your organization by a platform administrator before owners can set it up. ## Find the plugin Open the **Company Brain** group in the erxes sidebar: - **OpenClaw Assistant** — `/agent/assistant` - **Hermes Agent** — `/agent/hermes` - **Social / Postiz** — `/agent/postiz` - **Templates** — assistant templates you can start from ## Create your first assistant The OpenClaw and Hermes flows share the same identifier concept: you first create a named identifier (which reserves a plan slot), then the runtime is provisioned for it. ### Step 1 — Create the identifier 1. Open the module page and select **Add OpenClaw Assistant** (or the Hermes equivalent). 2. Choose **Setup type → Create new server** (or import transfer credentials to re-home an existing runtime). 3. Enter an **Identifier name** and a short **Description**, then Continue. 4. The identifier is created immediately and reserves one plan slot — even before the runtime exists. Deleting the identifier frees the slot. ### Step 2 — Choose the provider 1. Pick **API key** or **Provider subscription**. 2. Select a provider and complete its credential or authorization steps. 3. Confirm to start provisioning. Provisioning can take several minutes; the page polls the runtime health until it is serving. ### Step 3 — Open and share the assistant - Open the assistant detail page to chat with the runtime in a health-gated iframe — it only mounts once the runtime answers. - Invite team members from the assistant's member management. Members can open the runtime and check health; only owners and creators can edit, deploy, transfer, or delete. - Connect Discord from the assistant's Discord management sheet (optional). ## Set up Social / Postiz 1. Open **Social / Postiz** at `/agent/postiz`. 2. On erxes SaaS, wait until a platform administrator has approved your organization. On an enterprise installation, the owner enrolls directly with a single-use operator code. 3. The owner creates the tenant workspace with a separate Postiz password. 4. The owner selects the **initial Postiz Admin** from active erxes users. This one-time choice also applies to existing workspaces. 5. The initial Admin invites teammates as Admins or Members from the page header. Uninvited users never see the workspace. Full details: [Social / Postiz](https://erxes.io/docs/company-brain-postiz). ## What you can do next - Switch or update a provider connection at any time from the assistant's connection dialog — timeouts during apply mean "still applying", not failure. - Export **transfer credentials** to move an assistant to another erxes organization without losing data or provider connection. - Enable the CMS → Postiz publishing bridge from the Postiz Admin controls if your team creates content in the erxes CMS. If provisioning fails or access looks wrong, go to [Access and troubleshooting](https://erxes.io/docs/company-brain-troubleshooting). --- # OpenClaw Assistant Create an OpenClaw assistant, connect a provider, and manage its runtime. OpenClaw is the general-purpose Company Brain assistant. Each OpenClaw assistant is a dedicated managed runtime provisioned by erxes, connected to the model provider you choose, and reachable from the browser, from team members you invite, and optionally from Discord. ## What an OpenClaw assistant is - **One identifier = one runtime.** Creating the identifier reserves a plan slot and starts provisioning a dedicated server for your workspace. - **Isolated by design.** The runtime is reached only through erxes; the browser never talks to it directly. - **Provider-connected.** You connect an LLM provider with an API key or a provider subscription; the credential is forwarded to your runtime and is never stored by the plugin database. - **Shared with your team.** Owners see all identifiers; invited members can open the runtime and check health, while manage operations (edit, members, deploy, destroy, transfer, plan selection) stay with the owner or creator. ## Create an OpenClaw assistant 1. Open **Company Brain → OpenClaw Assistant** (`/agent/assistant`). 2. Select **Add OpenClaw Assistant**. 3. Choose **Setup type → Create new server** (or **Transfer** to import an existing runtime from another organization). 4. Enter an **Identifier name** and **Description**, then Continue. 5. Choose the connection type: - **API key** — paste a key from a supported provider, or - **Provider subscription** — use an existing subscription plan with a device-code, setup-token, or plan-key flow. 6. Select the provider and complete its fields, then confirm to provision. 7. Wait for provisioning to finish (it can take several minutes). If the provider requires an authorization step, the page guides you through it. The identifier is created before provisioning starts, so a failed deployment keeps the identifier (and its reserved slot) for a clean retry — your stored provider choice is reused unless you change it. ## Supported providers API-key providers: Kimi For Coding, Moonshot Kimi, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, DeepSeek, xAI Grok, GLM (Z.AI). Subscription plans: ChatGPT (device-code sign-in), Claude Pro/Max (Claude setup-token), GitHub Copilot (device code), MiniMax Coding Plan (device code), Qwen Coding Plan (plan key), Z.AI GLM Coding Plan (plan key). Each subscription option shows an in-app guide with the exact steps for that provider. Device-code providers open a provider authorization page with a one-time code; plan-key providers use a key you paste once. ## Use the assistant - **Chat** from the assistant page. The chat iframe is health-gated: it mounts only when the runtime answers, so restarts and key changes never show raw errors. - **Change provider or model** from the connection dialog whenever you need to — applying keeps working even if a check times out. - **Invite members** so teammates can use the assistant; view-only members can open the runtime and see health. - **Connect Discord** from the Discord management sheet: install the official erxes AI Assistant bot for your server, then bind a channel to this assistant. Channel bindings are assistant-scoped, and response controls let you choose which messages the assistant answers. ## Manage the runtime - **Health** is polled automatically; the detail page shows whether the runtime is serving. - **Retry provisioning** after a failure keeps the stored provider and credential mode unless you explicitly change them. - **Restart** from the toolbar when you need a fresh runtime process. - **Transfer** exports server credentials (name, URL, gateway token, IDs, provider, model, credential mode) that the create sheet in another organization can import. After transfer credentials are issued, destroying the source skips server teardown because the server may now be shared. - **Destroy** tears the runtime down. Deleting the identifier frees its plan slot. ## Quotas and billing OpenClaw and Hermes share one assistant entitlement: Business School 3, 10, and 25 People plans grant 3, 10, and 25 total assistant slots across both kinds. The limit query reports the shared limit, usage, and remaining slots; over-plan assistants use the billing selector. Enterprise installations keep their unlimited behavior. ## Related pages - [Getting started](https://erxes.io/docs/company-brain-getting-started) - [Hermes Agent](https://erxes.io/docs/company-brain-hermes) - [How it works](https://erxes.io/docs/company-brain-architecture) - [Access and troubleshooting](https://erxes.io/docs/company-brain-troubleshooting) --- # Hermes Agent Provision an isolated Hermes runtime and connect chat, tools, and Discord. Hermes is Company Brain's second runtime kind: a fully isolated agent runtime per assistant, running in its own Kubernetes namespace with persistent storage. Use Hermes when you want durable workspace state, erxes workspace tools, and the shared skill pot alongside chat and Discord. ## What a Hermes agent is - **One identifier = one isolated runtime.** Creation first makes a `hermes` identifier (reserving a plan slot), then provisions a dedicated runtime while Kubernetes reconciles it. - **Persistent.** The runtime keeps its home and workspace on persistent volumes, so provider credentials and assistant state survive restarts — ordinary restarts do not require signing in again. - **Separate from OpenClaw.** Hermes has its own fleet view, its own runtime records, and its own lifecycle. Deploying or destroying a Hermes agent can never mutate an OpenClaw assistant. - **Private by default.** The browser reaches the runtime only through a backend-minted, minutes-long login URL; the runtime's network policy admits nothing else. ## Create a Hermes agent 1. Open **Company Brain → Hermes Agent** (`/agent/hermes`). 2. Select **Create** and choose **Setup type → Create new server**. 3. Enter the **Identifier name** and **Description**, then Continue. 4. Pick the connection type: - **API key** — paste a key from a supported provider, or - **Subscription plan** — choose one of the six Hermes subscription plans. 5. Complete the provider step and confirm. After creation, the runtime boots in its own namespace. **ChatGPT and MiniMax** plans continue into a one-time device-code step after the pod is ready; token and setup-key plans (Claude setup-token, GitHub token, Qwen and Z.AI plan keys) are applied during deployment. The page polls while Kubernetes finishes the rollout. ## Hermes subscription plans Hermes accepts API keys from the same major providers as OpenClaw, plus six subscription options with Hermes-specific guides: - **Claude** — a setup-token generated by Claude Code on your own computer. - **ChatGPT** — a one-time device code; no API key needed. - **GitHub Copilot** — a GitHub token from an account with an active Copilot plan. - **MiniMax Coding Plan** — device-code sign-in; no API key required. - **Qwen Coding Plan** — the global Coding Plan key. - **Z.AI GLM Coding Plan** — the global Coding Plan key. After connection, the credential is stored only in that assistant's private runtime on its persistent volume. ## Use the Hermes WebUI - Open the runtime from the fleet card or detail page. erxes mints a short-lived login URL per user; raw runtime credentials never reach the browser. - Copy the WebUI password from the detail page when you need a direct login. - Health is polled continuously; the fleet card shows provisioning state (deploying, ready, failed) at a glance. ## Connect erxes workspace tools Hermes runtimes can call erxes capabilities through a scoped OAuth client: 1. From chat, start the erxes connection (`erxes_auth_start`) — it returns an approval URL and one-time code. 2. Approve in erxes, selecting the permission actions you want to grant. 3. `erxes_tools_search` then lists only the live capabilities your OAuth scopes and erxes permissions allow, and `erxes_tool_call` executes them. Destructive capabilities ask for a one-time confirmation first. The plugin can only reach the agent-tools manifest and call endpoints — it cannot send arbitrary GraphQL — and OAuth tokens stay in the runtime's persistent state. Replacing or removing the connection invalidates saved credentials. ## Use the skill pot The fleet shares a skill registry: - **Community skills** are published by runtimes like yours and are visible only inside the organization that published them. - **Verified skills** are promoted by erxes and visible to every customer. Inside the runtime, ask the assistant to search the pot before building, and publish with a green self-test (`erxes-skill-publish <skill>` runs the skill's self-test, a safety scan, then publishes). ## Connect Discord Ready Hermes runtimes expose the same Discord management sheet as OpenClaw, from the detail toolbar and fleet card. The official erxes AI Assistant bot installation is shared by both runtime kinds, but each channel binding is assistant- and runtime-scoped. The sheet opens automatically after creation and provides server/channel selection and response controls. ## Transfer and delete - **Transfer credentials** export the controller-verified runtime token and connection metadata. Importing them in another organization re-homes the existing runtime — preserving its persistent volumes — instead of creating a replacement. - **Delete** destroys the runtime's Kubernetes resources; the persistent volume is retained per the storage policy. ## Quotas Hermes shares the Company Brain assistant entitlement with OpenClaw: 3, 10, and 25 People plans grant 3, 10, and 25 total slots across both kinds, and the fleet view shows your remaining capacity. Creation is blocked while the entitlement is exhausted; over-plan assistants use the billing selector. ## Related pages - [Getting started](https://erxes.io/docs/company-brain-getting-started) - [OpenClaw Assistant](https://erxes.io/docs/company-brain-openclaw) - [How it works](https://erxes.io/docs/company-brain-architecture) - [Access and troubleshooting](https://erxes.io/docs/company-brain-troubleshooting) --- # Social / Postiz Connect a social workspace, manage access, and prepare content for publishing. This guide covers how the erxes company brain connects to Postiz for social media publishing. The Postiz integration lives under **Social / Postiz** at `/agent/postiz` in your workspace. ### Who can enable Postiz - On the shared SaaS deployment, Postiz enrollment requires approval from a platform admin. - A workspace owner cannot enroll the shared deployment on their own; the request must be approved first. - Enterprise owners do not need this approval for their own deployment. ### Enterprise enrollment An enterprise owner connects Postiz from the Social / Postiz page. - The erxes origin must be a verified HTTPS origin. - Connect generates a single-use operator code. - The operator code expires 15 minutes after it is issued. - Use the code during that window; if it expires, generate a new one. ### Workspace creation in Postiz After connecting, the owner creates the Postiz workspace. - Choose a workspace name in Postiz. - Set a separate Postiz password, distinct from any erxes password. - The Postiz login email is derived from the server, not chosen by the owner. - If enrollment is retried, workspace placement in Postiz is preserved; retries do not create duplicate workspaces. ### Initial erxes Postiz Admin The owner selects the initial active erxes Postiz Admin during enrollment. - The admin can be any active tenant user, including an existing workspace member. - Selecting a user as Postiz Admin does not give the owner implicit access to Postiz. If another user is selected, the owner has no Postiz access through their ownership role alone. - The initial admin choice is immutable after enrollment. - Admins cannot change their own access level in Postiz. ### Team membership The erxes Postiz Admin invites active tenant users to Postiz. - Use **Invite team member** to add users with the Admin or Member role. - Only active tenant users can be invited. - Membership is managed inside Postiz after the initial setup. ### Access boundaries Postiz accounts are separate from erxes accounts. - erxes access does not create Postiz accounts and does not enable SSO into Postiz. - Postiz login and membership are managed separately from erxes users and permissions. - Provider OAuth is configured at the top level of Postiz, not per member. ### Publishing social content Social channels require an operator-configured provider app before they can be connected. - Each social network needs provider app configuration and approval by the operator. - Team members create drafts in Postiz, then submit them for review. - After review approval, content is scheduled in Postiz. - Check delivery status in Postiz after the scheduled time; confirm the post was delivered, not just that it was submitted. ### CMS publishing bridge The CMS can publish content to Postiz, but this is disabled by default. - An admin must enable `cmsPublishingEnabled` to turn on CMS publishing. - CMS content permissions are independent of Postiz permissions. - The bridge is a signed server-to-server flow: `content_api` sends to `agent_api`, which forwards to the gateway, which reaches Postiz. - The CMS bridge is not an agent tool; it never runs through the assistant runtime. - `JWT_TOKEN_SECRET` must match between the CMS side and the agent side for the signed bridge to verify. - The legacy `CMS_POSTIZ_SHARED_SECRET` is retired; matching `JWT_TOKEN_SECRET` is the only supported bridge secret. ### Expiry and revocation When the operator code expires or access is revoked: - erxes access to Postiz is removed. - Direct Postiz sessions are not terminated. - Existing Postiz schedules are not removed. - Postiz data stays in place. - Only the erxes-side link is affected. ### Related pages - [Company brain introduction](https://erxes.io/docs/company-brain-introduction) - [Company brain architecture](https://erxes.io/docs/company-brain-architecture) - [Company brain troubleshooting](https://erxes.io/docs/company-brain-troubleshooting) --- # How it works Explore plugin boundaries, provisioning, tenant isolation, and integration contracts. This page explains what happens under the hood of Company Brain: the plugin layout, how runtimes and workspaces are provisioned, and how access is enforced. It is written for developers and operators; end users can stay on the module guides. ## Plugin boundaries Company Brain is delivered by the `agent` plugin, which pairs a backend API and a frontend UI: - `agent_api` — a backend plugin microservice registered with the erxes gateway through service discovery. It owns the assistant lifecycle, LLM connection management, runtime proxying, Discord gateway integration, assistant organization identifiers, and the Postiz module. - `agent_ui` — a private frontend plugin loaded by the erxes core host via Module Federation. It provides the Company Brain navigation, the provisioning wizards, chat surfaces, fleet views, Discord sheets, and the Social / Postiz page under `/agent/*`. The plugin follows the standard erxes plugin contract: its backend owns its data model per tenant subdomain, exposes GraphQL through federation and tRPC for service-to-service calls, and its frontend remote is consumed by core-ui. No other plugin's code is imported; shared platform capabilities are used through public interfaces. ## Module topology ### OpenClaw Assistant - Each OpenClaw assistant is one managed runtime. The plugin asks the managed deployer to provision it; the deployer selects the Kubernetes provisioner when enabled and otherwise falls back to the legacy VM provisioner. - Runtimes are reached only through authenticated runtime endpoints and the deployer; health probes gate the chat surface. - Discord traffic flows through the assistant gateway, which signs tenant and assistant identity before it reaches a runtime adapter. ### Hermes Agent - A separate controller reconciles one isolated Kubernetes namespace per Hermes runtime: its own Deployment, Service, Secrets, ConfigMap, and two persistent volumes. - The controller also proxies browser access (minted login URLs, identity header injection) and the signed Discord adapter endpoint on the same runtime URL. - Provider API keys and subscription credentials are forwarded into the runtime's Kubernetes secrets; they are never persisted by the plugin database, logs, or GraphQL responses. ### Social / Postiz - Postiz is not a per-assistant pod. Your tenant receives a workspace on a gateway-assigned Postiz installation, created through an authenticated provisioning endpoint. - erxes-side access is stored in a tenant-owned access record; placement is re-checked on every request, and the embedded page rechecks access periodically and on tab focus. ## Provisioning chains ### OpenClaw 1. The UI collects the identifier, provider, and credential mode. 2. The plugin creates the identifier inside a Redis quota lock, reserving a plan slot before any infrastructure work. 3. `deployManagedAgent` calls the deployer with a deployer-only secret header. Deployment waits (up to a 10-minute timeout) for the runtime to become ready. 4. The plugin records pending, deploying, approved, or failed state and refreshes live gateway tokens for approved runtimes. 5. Retries reuse the stored provider and credential mode unless the caller explicitly sends new values. ### Hermes 1. The identifier is created the same way (kind `hermes`). 2. The plugin posts to the deployer's Hermes relay, which forwards to the in-cluster controller with its own deployer secret. The controller fails closed if its secret is not configured. 3. The controller creates the namespace, volumes, and workload, then waits on rollout. Deploys wait up to 10 minutes; other operations return quickly. 4. A shared assistant-runtime secret is delivered through the chain so the assistant gateway can verify signed tenant and assistant identity at the runtime adapter. ### Postiz 1. The owner's placement request checks active core membership, then sends an expiring HMAC assertion to the Postiz gateway. 2. The gateway reserves one instance per tenant and creates the Postiz workspace through its authenticated provisioning endpoint; retries return the existing placement. 3. The team-access record is initialized with one initial admin; member updates use compare-and-set to reject stale concurrent writes. ## Identity, access, and isolation - **Identifiers** (`assistant_orgs`) carry a name, unique slug, kind (`assistant` or `hermes`), description, and member list. Owners see all identifiers; other users only those they created or were invited to. - **Manage operations** — edit, members, deploy, destroy, transfer, plan selection — require the owner or creator. View-only members can open the runtime and poll health (and mint Hermes login URLs) but nothing else. - **Tenancy.** Models are scoped per subdomain; every request carries tenant context, and Postiz access fails closed across tenants (a user ID from another tenant cannot grant access). - **Secrets handling.** Provider keys pasted in the UI are normalized, used transiently, and never persisted in plugin state. Deployer and runtime secrets are never returned to clients. Postiz passwords are forwarded once and never stored. - **Quota serialization.** Identifier creation and plan selection run inside a short Redis lease keyed per tenant, so concurrent creations cannot overbook a plan. ## CMS ↔ Postiz bridge When a Postiz Admin enables sharing, approved CMS content can be handed to Postiz through a signed server-to-server chain: `content_api` → `agent_api` → Postiz gateway → Postiz. The envelope is signed with a key derived from the shared JWT secret and carries tenant, user, action, and payload with a Redis nonce for replay protection. The bridge is not an agent tool and never exposes the assistant runtime; content_api still enforces its own CMS permissions. ## What the plugin does not own - The deployer and controller services (separate components with their own deployment) — the plugin only calls their HTTP APIs. - The runtime pods and Postiz instances themselves. - Postiz accounts, provider OAuth apps, or email delivery — those remain external systems. Continue to [Setup and Kubernetes](https://erxes.io/docs/company-brain-setup) for deployment topology and configuration, or back to [Social / Postiz](https://erxes.io/docs/company-brain-postiz) for the user-facing flow. --- # Setup and Kubernetes Configure the plugin, deployers, Kubernetes runtimes, and Postiz installation. This page is for developers and operators enabling the Company Brain plugin on an erxes installation, including the Kubernetes runtimes behind OpenClaw and Hermes and the Postiz installation. End users can skip to [Getting started](https://erxes.io/docs/company-brain-getting-started). ## What you are deploying Company Brain is not one container. Expect four cooperating layers: - `agent_api` — the plugin backend (GraphQL via the gateway, tRPC, MongoDB). - `agent_ui` — the private Module Federation remote consumed by core-ui. - Assistant runtimes — one isolated runtime per OpenClaw or Hermes assistant. - The Postiz gateway with its assigned Postiz instance(s). ### OpenClaw runtimes OpenClaw assistants run on a Kubernetes cluster. The deployer chooses the Kubernetes provisioner when `USE_K8S_PROVISIONER=true`; otherwise it falls back to the legacy per-VM provisioner. Each assistant becomes a Deployment, Service, Secret, and Ingress entry in the cluster, with a pinned OpenClaw image. ### Hermes runtimes Hermes uses a separate in-cluster controller (two replicas) that renders runtime configuration and drives Kubernetes directly. Each Hermes assistant gets its own namespace with a Deployment, Service, Secrets, a ConfigMap, and two persistent volumes (home and workspace). The controller proxies the WebUI and the signed Discord adapter on the same runtime URL, backed by a retain-class storage policy. ### Postiz Postiz is a workspace on a shared, gateway-managed Postiz installation — not a pod per tenant. The gateway reserves one instance per tenant and exposes the workspace through your erxes page. ## Prerequisites - An authorized erxes source checkout (the agent plugin is private). - The existing platform stack: MongoDB, Redis, the API gateway, and core-api. - A Kubernetes cluster with cluster credentials, persistent storage (a storage class whose volumes are retained), DNS, and TLS ingress for runtime URLs. - A secret manager or encrypted deployment-secret workflow — never commit credentials. - For Postiz: a patched Postiz backend and the gateway with its registry of approved instances. ## Enable the plugin locally From the erxes repository root: ```sh pnpm install cp .env.sample .env # add agent to ENABLED_PLUGINS pnpm nx build agent_api pnpm nx build agent_ui pnpm nx serve agent_api # in a second terminal pnpm nx serve agent_ui ``` The UI is consumed by core-ui through Module Federation; run the existing core stack (gateway, core-api, core-ui) as you normally do. The plugin's backend listens on its own port (33010) and registers with the gateway through Redis service discovery. ## Environment variables Names only — provision values through your secret manager. ### agent_api - `DEPLOYER_URL` — managed deployer base URL. - `MANAGED_OPENCLAW_DEPLOYER_SECRET` — deployer authentication header value. - `ERXES_AI_ASSISTANT_RUNTIME_SHARED_SECRET` — shared runtime secret; must match what the assistant gateway and runtimes use. - `DOMAIN`, `MONGO_URL`, `REDIS_HOST`, `REDIS_PORT` — existing platform settings. - `JWT_TOKEN_SECRET` — stable, at least 32 characters; also derives the CMS bridge key for Postiz. ### Managed deployer (OpenClaw) - `MANAGED_OPENCLAW_DEPLOYER_SECRET` — must match agent_api. - `USE_K8S_PROVISIONER` — set `true` for Kubernetes runtimes. - `OPENCLAW_IMAGE` — pinned runtime image. - Cluster credentials, proxy domain, ingress scheme/TLS secret, and storage settings per your deployer deployment; see the deployer documentation. ### Hermes controller - `HERMES_K8S_DEPLOYER_URL` and `HERMES_K8S_DEPLOYER_SECRET` — how the deployer relay reaches the controller. - The controller requires its deployer secret at startup and fails closed without it; runtime images are pinned per release. ### Postiz Enterprise installations keep one permanent setting on agent_api: - `POSTIZ_GATEWAY_ORIGIN` — the Postiz gateway HTTPS origin. plus the existing `DOMAIN` (HTTPS origin) and a stable `JWT_TOKEN_SECRET` (32+ characters). Enterprise enrollment credentials live encrypted in the plugin-owned `agent_postiz_installations` collection; back it up together with `JWT_TOKEN_SECRET` and re-encrypt before rotating that secret. Shared SaaS deployments are configured permanently through deployment secrets instead: - `POSTIZ_MANAGEMENT=saas` - `POSTIZ_INSTALLATION_ORIGIN` — restricted to the approved managed base origins - `POSTIZ_INSTALLATION_ID` - `POSTIZ_GATEWAY_SIGNING_KEY` plus the gateway URL. SaaS mode never reads the encrypted credential store and never falls back to it; missing or partial credentials leave Postiz unconfigured. ## Rollout order 1. Deploy infrastructure first: Postiz backend and gateway, the deployer, and the Hermes controller, in whichever order your runbook requires. 2. Deploy `agent_api` and verify the federated schema exposes the Company Brain operations. 3. Publish `agent_ui` last — the UI requires the newer API contracts and fails against older APIs. 4. For the CMS → Postiz bridge, coordinate `content_api` and `agent_api` in the same window; old and new envelope formats are incompatible. ## Smoke checks - Create an assistant of each kind; confirm provisioning completes and health turns serving. - Invite a member and confirm they can open the runtime but not manage it. - Confirm a non-member cannot open the runtime or see a workspace URL. - Complete a provider connection (API key and subscription flow) and verify chat works; sign in again is not required after a runtime restart. - For Postiz: owner setup, initial admin selection, member invite, and a non-member denial without any workspace URL exposure. - Restart a Hermes runtime and confirm state and credentials persist. ## Backups and lifecycle - Back up `agent_postiz_installations` and `JWT_TOKEN_SECRET` together for enterprise; back up SaaS deployment secrets alongside the gateway's keys and registry. - Retained storage means destroyed Hermes runtimes leave volumes behind — include them in your storage cleanup and backup procedures. - Never roll the gateway below its minimum supported version after enrollment or revocation; older versions ignore the newer credential controls. Continue to [Access and troubleshooting](https://erxes.io/docs/company-brain-troubleshooting) and [How it works](https://erxes.io/docs/company-brain-architecture). --- # Access and troubleshooting Resolve provisioning, provider, quota, Discord, and Postiz access problems. Common Company Brain problems and how to resolve them. If your symptom is not here, check [How it works](https://erxes.io/docs/company-brain-architecture) for the expected behavior of each flow. ### Provisioning fails or stays pending - Provisioning can take several minutes; the page polls the runtime until it is serving. Wait for the timeout before retrying. - A failed deployment keeps the identifier and its reserved plan slot; use the retry action. Retry reuses the stored provider and credential mode unless you explicitly change them. - If a subscription provider needs a re-authorization step, the retry card shows the provider's credential prompt (or no input for device-code providers). ### The chat area shows "Connecting…" for a long time - The chat surface only mounts once the runtime answers its health check. Recent key changes, restarts, or pod recreation keep the overlay up briefly by design. - If it never mounts, retry provisioning from the assistant page, then re-open the assistant. ### Provider subscription authorization fails - Device-code providers (ChatGPT, GitHub Copilot, MiniMax) open a provider page with a one-time code; codes expire, so restart the flow if it stalls. - Claude subscriptions need a setup-token generated by Claude Code on your own computer — an API key will not work. - Plan-key providers (Qwen, Z.AI) need the key issued for the global Coding Plan, not the standard pay-as-you-go API key. - Applying a connection that times out or returns a service error may still be in progress; re-open the connection dialog to check the current state instead of re-submitting immediately. ### "Limit reached" when creating an assistant - OpenClaw and Hermes share one assistant entitlement. Business School 3, 10, and 25 People plans grant 3, 10, and 25 total slots across both kinds. - Delete an identifier you no longer need to free its slot; identifiers created before a failed deployment still occupy a slot until deleted. - Over-plan assistants use the billing selector; enterprise installations are unlimited. ### A team member cannot open an assistant - Only owners see all identifiers; members must be invited to the specific identifier. - View-only members can open the runtime and see health, but manage actions (edit, members, deploy, destroy, transfer) stay with the owner or creator. - After access changes, the member's open pages pick it up on the next check; a reload forces it. ### Discord is connected but the assistant does not respond - Confirm the channel binding exists for this assistant — bindings are assistant- and runtime-scoped, so connecting a channel to one assistant does not serve another. - Confirm the runtime is healthy; the Discord path needs a serving runtime. - Response controls on the Discord sheet decide which messages get answers; check the configured response mode. ### Postiz: "not enabled" or access denied - On erxes SaaS, an organization must be approved by a platform administrator before owners can set up Postiz. Ask your platform contact. - Uninvited users see guidance only — the workspace URL is never exposed. - Access is rechecked periodically and on tab focus; removed access unmounts the embedded workspace on the next check, not instantly. ### Postiz: owner setup or password issues - Only the active organization owner can create the tenant workspace; the login email is derived server-side and the Postiz password is separate from any erxes password. - The password field clears after submission and is never stored by erxes; use Postiz's own recovery if the password is lost. - The initial Postiz Admin choice is one-time and immutable; if the initial admin was deactivated, recovery requires a separately authorized operator procedure — there is no automatic owner takeover. ### Postiz: invitations and roles - Admins invite active erxes users as Admin or Member from the header's **Invite team member** sheet; owners have no bypass after initialization. - The initial admin cannot be removed or demoted, and admins cannot change their own role. - erxes access does not sign anyone into Postiz — normal Postiz login and membership still apply, and provider connections are configured in Postiz itself. ### CMS content does not reach Postiz - Sharing is disabled by default; a current Postiz Admin must enable it before members can share. - content_api permissions (assignment, publishing, sharing, author scope, language) apply independently — the bridge never bypasses them. - The bridge verifies matching JWT-derived signing keys on both APIs; if `content_api` and `agent_api` were upgraded at different times, envelopes fail until both are current. ### Enterprise Postiz enrollment problems - Enrollment codes are single-use and expire in 15 minutes; request a new one after expiry. - Retries keep the installation identity — a lost response does not create a duplicate installation. - Enrollment is initial-only: once team access is initialized there is no self-service re-enrollment or rotation; credential changes need the operator recovery procedure. ### A workspace disappeared from the erxes page after expiry - Expired or revoked access removes only the erxes-side link. Direct Postiz sessions, schedules, and workspace data are untouched, and renewal restores the same workspace. ### Still stuck? - [Getting started](https://erxes.io/docs/company-brain-getting-started) — setup walkthrough - [Setup and Kubernetes](https://erxes.io/docs/company-brain-setup) — operator configuration - [Social / Postiz](https://erxes.io/docs/company-brain-postiz) — the full Postiz flow - [Community and support](https://erxes.io/docs/introduction) — Discord and support channels --- # Contributing Ways to contribute to erxes: code, docs, issues, security reports, and licensing notes erxes accepts code, documentation, tests, translations, bug reports, and security reports. ## Ways to contribute | Contribution | Where it goes | Start here | | --- | --- | --- | | Bug report or feature request | [GitHub Issues](https://github.com/erxes/erxes/issues) | Search first. Include the erxes version (root `package.json` `version`), the affected plugin, reproduction steps, and expected vs. actual behavior. | | Code: fix, feature, or plugin | Pull request to `erxes/erxes` | Finish [Local Setup](https://erxes.io/docs/local-setup) first, then [Contribute to codebase](https://erxes.io/docs/contribute-codebase) for the workflow, [Code Standards](https://erxes.io/docs/contribute-standards), and [Testing](https://erxes.io/docs/contribute-testing). New plugins start with `pnpm create-plugin`; see [Create a Plugin](https://erxes.io/docs/create-plugin). | | Documentation | `erxes/erxes-global-profile`, `markdown/*.mdx` | See [How the public docs are maintained](#how-the-public-docs-are-maintained). | | Translations | `backend/gateway/src/locales/<lng>/<namespace>.json`, or a plugin's own `src/locales` (for example [`frontline_api/src/locales`](https://github.com/erxes/erxes/tree/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/locales)) | The gateway serves these files at `/locales/:lng/:file`, falling back to files fetched from plugin services, and `core-ui` loads them with `i18next-http-backend` ([config](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/src/i18n/config.ts)). Supported languages are `en` and `mn`. `CONTRIBUTING.md` points existing translation work to [Transifex](https://explore.transifex.com/erxes-inc/erxesxos/). | | Questions | [Discord](https://discord.com/invite/aaGzy3gQK5) or a GitHub issue | Ask before you introduce a new pattern. | The repository has no issue or pull request templates: `.github/` contains only `workflows/`. The conventions above come from [`CONTRIBUTING.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CONTRIBUTING.md), not from a form. Issues labeled `bug` or `sentry` are forwarded to an internal listener by [`auto-issue-webhook.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/auto-issue-webhook.yml), so label bug reports accurately. [`HACKTOBERFEST.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/HACKTOBERFEST.md) is a welcome note for Hacktoberfest 2025. It repeats the branch prefixes (`fix/`, `docs/`, `feat/`) and asks for quality over quantity; it adds no separate process. ## Code of conduct [`CODE_OF_CONDUCT.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CODE_OF_CONDUCT.md) is the Contributor Covenant 1.4. It applies in project spaces and wherever you represent the project. Report abusive behavior to `info@erxes.io`; maintainers may remove contributions or ban contributors who violate it. ## Report a security vulnerability > [!WARNING] > **Do not open a public issue for a vulnerability** > > Email `info@erxes.io` with a description, the steps you took, affected versions, and known mitigations. [`SECURITY.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/SECURITY.md) states a 90-day disclosure timeline. The repository has no private advisory form beyond this email. ## License implications for contributors - [`LICENSE.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/LICENSE.md) places everything under **AGPLv3** except plugins whose name ends in `-ee` under an `ee/` directory (Enterprise Edition, governed by `ee/LICENSE`; see https://erxes.io/pricing/enterprise-edition) and third-party components under their own licenses. - `LICENSE.md` also states that erxes may not be hosted as a SaaS product that competes with erxes Inc. - The root `package.json` declares `"license": "MIT"`. This contradicts `LICENSE.md` and the AGPLv3 badge in `README.md`; treat `LICENSE.md` as the governing text and ask maintainers if the distinction matters for your use. - There is no CLA or DCO file. Unless a maintainer states otherwise, your pull request is contributed under the repository license. ## How the public docs are maintained The pages on this site are MDX files in [erxes/erxes-global-profile](https://github.com/erxes/erxes-global-profile) under `markdown/`, one file per slug. A page is published only when it is registered in `constants/docs.ts` and imported in `markdown/registry.ts`; adding a file alone does nothing. When you edit a page, check every path, command, and operation name against the code it links. ## Guides - [Contribute to codebase](https://erxes.io/docs/contribute-codebase): Fork, branch, validate with Nx, and open a pull request against erxes/erxes - [Code Standards](https://erxes.io/docs/contribute-standards): Boundaries, types, and UI rules - [Testing](https://erxes.io/docs/contribute-testing): Project targets and manual flows - [Releases](https://erxes.io/docs/contribute-releases): Versions and image promotion --- # Contribute to codebase Fork, branch, validate with Nx, and open a pull request against erxes/erxes Fork, branch, validate with Nx, and open a pull request against `erxes/erxes`. The rules your code must follow live in [Code Standards](https://erxes.io/docs/contribute-standards) and [Testing](https://erxes.io/docs/contribute-testing). ## Prerequisites - Node.js 22 and pnpm 8 or newer; the root `package.json` pins `pnpm@9.12.3` in `packageManager` and blocks npm and Yarn through `engines`. MongoDB, Redis, and Elasticsearch 7 (only when the feature needs search) are also required. - A working local checkout. Finish [Local Setup](https://erxes.io/docs/local-setup) before you start. - Agreement on scope: search [existing issues](https://github.com/erxes/erxes/issues) first, and discuss large features, architectural changes, or new dependencies with maintainers before writing code. Pull requests target the `develop` branch. ### 1. Pick an issue and find the project to change Choose an existing issue or open one with the current behavior, expected behavior, and reproduction context. Keep one pull request to one issue or one cohesive outcome. Then identify which Nx project holds the change: `backend/plugins/<name>_api` for a backend plugin, `frontend/plugins/<name>_ui` for its frontend, `backend/core-api` / `frontend/core-ui` for core work. Do not move logic into a shared library or another plugin because it is convenient. ### 2. Fork, clone, and branch from develop ```bash git clone https://github.com/<your-github-username>/erxes.git cd erxes git remote add upstream https://github.com/erxes/erxes.git git fetch upstream git switch develop git pull --ff-only upstream develop git switch -c fix/deal-stage-filter pnpm install ``` [`CONTRIBUTING.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CONTRIBUTING.md) requires the branch prefixes `feat/` (features), `fix/` (bug fixes), and `docs/` (documentation), followed by a short description such as `fix/deal-stage-filter`. ### 3. Read the local conventions Each directory's `AGENTS.md` describes its local conventions; read the ones on the path to the files you will change. Plugin guides live at the root of `backend/plugins/<name>_api/` and `frontend/plugins/<name>_ui/`; read the guide for the plugin in scope, not every plugin's. The closest file governs local details, and local rules never weaken the plugin scope boundary, security, or tenant isolation. ### 4. Stay inside the plugin scope boundary A backend-only task may write only under `backend/plugins/<name>_api/**`; a frontend-only task only under `frontend/plugins/<name>_ui/**`; a full-stack plugin task only under those two directories. Never edit `core-api`, `core-ui`, another plugin, a shared library, or root infrastructure to make a plugin feature work, and never import another plugin's source. Use shared code only through the public APIs of `erxes-ui`, `ui-modules`, or `erxes-api-shared`. If the behavior cannot be built inside the boundary, stop and report the missing platform capability instead of widening the diff. ### 5. Implement, then commit Search for a similar implementation in the same project first and reuse its components, hooks, GraphQL documents, model conventions, and error handling. Deliver complete behavior: no placeholder pages, inactive buttons, forms without validation, lists without loading or empty states, or mutations without feedback. `CONTRIBUTING.md` does not enforce a commit-message format, but the history follows conventional commits: `feat(scope): ...`, `fix(scope): ...`, occasionally `release X.Y.Z` (written by release-it). Match that pattern. [`release-it`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.release-it.json) generates `CHANGELOG.md` from conventional messages via `@release-it/conventional-changelog`, so scoped messages feed the published changelog. Keep commits small and coherent, and reference the issue number. ### 6. Run the required checks locally Run the targets the changed project defines; inspect `project.json` or `pnpm nx show project <name>`: ```bash pnpm nx lint sales_ui pnpm nx build sales_ui pnpm nx test sales_ui # only when a test target exists pnpm nx affected -t lint,build,test # for changes spanning projects ``` When you change `erxes-api-shared`, rebuild it before validating consumers: `pnpm nx build erxes-api-shared`. `content_api` defines no Nx `lint` or `test` target; it has its own validation commands, covered in [Testing](https://erxes.io/docs/contribute-testing). On your pull request, the per-area workflows (`CI plugin--<name>_api`, `CI plugin--<name>_ui`, `core-ui-ci`, `CI Gateway`, `CI migrations`, plus `CodeQL` on PRs to `main` and `develop`; see [ci-api-core.yml](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/ci-api-core.yml)) install dependencies and build the affected project. Docker pushes and R2 syncs are gated to pushes on `main` and tags, so a PR gets build checks, not publishes. ### 7. Open the pull request Open the PR against `develop`, link the issue, and include what changed, why, how, the verification commands you ran, screenshots or recordings for visible UI changes, and migration or deployment notes when operators must act. Rebase and resolve conflicts locally before requesting review. Pull requests are squash-merged, and maintainers create releases from the integration branches; contributors do not update release metadata. --- # Code Standards Boundaries, types, and UI rules The conventions reviewers apply to every change in `erxes/erxes`, drawn from the repository's rule files and the lint and formatter configuration. For the fork-and-PR workflow, see [Contribute to codebase](https://erxes.io/docs/contribute-codebase). ## How the rules are organized The root `AGENTS.md` is the repository-wide rule file, and each directory can carry its own. Nested files may add constraints or pick among approved patterns, but they cannot weaken the non-negotiable rules, the plugin scope boundary, security requirements, or tenant isolation. When several guides apply to a file, all of them apply. When rules conflict with preference, the working principles set a priority order: existing repository patterns first, then local plugin consistency, minimal changes, clear behavior and maintainability, reusability, performance, and personal preference last. If you are unsure whether a change violates a rule, ask rather than guess. ## Non-negotiable rules These must hold in any new or modified code. | Rule | Why | How to check | | --- | --- | --- | | Use `erxes-ui` and `ui-modules` components; never import `@radix-ui/*` directly or hand-roll primitives; prefer composed APIs (`Form.Field`, `RecordTable.Provider`, `Sheet.Content`) | One design system; primitives already wrap Radix | Grep the diff for `@radix-ui` imports | | After a create/update/delete mutation, update the UI immediately through Apollo cache, refetch, or `subscribeToMore` | Users must never need a manual refresh | Exercise the mutation in the browser without reloading | | Completeness: every button has a handler, every form validates, every list has loading and empty states, every mutation reports success/error; no placeholder code | Half-finished pages ship bugs to self-hosters | Click through the whole flow in each state | | Named exports in application code; a default export only where an external tool (Nx, Rspack, Module Federation loader) requires it | Named exports survive refactors and are what host loaders import | Review exports in the diff | | No `any`, `as any`, or unnecessary casts in new or modified code; type all props and hook returns | The ESLint rule `@typescript-eslint/no-explicit-any` is turned **off** in `eslint.config.js`, so type safety is a review rule, not a lint rule | `pnpm exec tsc --noEmit` on the project; read the diff | | GraphQL operations are named, unique repository-wide, and prefixed with the plugin or module (`cmsPageList`, `salesDealCreate`), colocated with the feature | Operation names are public API used by caching, logging, and codegen | Search the repo for the operation name before adding it | | Backend schemas follow their module's established `new Schema(...)` and `schemaWrapper` pattern; never change a backend API from a frontend-only change | Tenant models and shared schema behavior are load-bearing | Compare with a sibling module in the same plugin | | Plugin isolation: a plugin change stays inside that plugin; no cross-plugin imports; no edits to core, shared libraries, or root infrastructure | Plugins must remain independently buildable, testable, deployable, and removable | `git diff --stat` shows only allowed directories | | No leftover `console.log`/`debugger`, commented-out code, unused imports/variables, or untracked `TODO`s | Dead code rots fast in a monorepo | `pnpm nx lint <project>` plus a final diff read | ## Forbidden changes - Introducing a new UI system or replacing existing patterns with personal preferences. - Casually renaming public APIs, GraphQL operation names, routes, or Module Federation exposes. - Moving Module Federation exposes without updating every host reference. - Large refactors without an explicit request. - Changing core, shared libraries, root infrastructure, or another plugin as an implicit part of plugin work; that is a separate, explicitly scoped task. - New dependencies the change does not require, or ones an existing platform package already covers. ## Frontend plugin rules For work under `frontend/plugins/<name>_ui`: | Area | Rule | | --- | --- | | Project shape | Route-level pages live in `src/pages` or `src/modules` following the touched plugin's pattern; feature internals stay near the feature in `src/modules`; `src/widgets` is only for Module Federation widget exports. Shared primitives go to `frontend/libs/erxes-ui`, shared product modules to `frontend/libs/ui-modules`; never to `core-ui`. | | Imports | Use local imports or the plugin aliases (`~/*` for `src`, `@/*` for `src/modules`); no cross-plugin imports; move UI to a shared lib only for clear cross-plugin reuse. | | UI | Match nearby layout, spacing, loading/empty states, filters, and action placement; reuse `erxes-ui`/`ui-modules` tables, forms, sheets, dialogs, and filters; icons come from `@tabler/icons-react`; preserve responsive behavior; no inline styles unless neighbors use them. | | React | Functional components and existing hooks; Jotai only for shared sibling/page state or existing atom-backed patterns; keep hooks, constants, types, and GraphQL documents next to the feature. | | GraphQL | Search existing documents first; prefix names with the plugin/module and keep them unique; reuse the plugin's fragments and Apollo hooks; do not change backend APIs from frontend work unless explicitly requested. | | Module Federation | Check `module-federation.config.ts` before exposing modules; the named-export rule applies to every expose; never satisfy a host loader with a `default` export; fix the expose or the loader instead. | ## Lint and formatting - ESLint uses a flat config rooted at [`eslint.config.js`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/eslint.config.js): the Nx `flat/base`, `flat/typescript`, and `flat/javascript` presets, plus `@nx/enforce-module-boundaries` set to `error`. Its options are `enforceBuildableLibDependency`, `allowCircularSelfDependency`, and a `depConstraints` entry allowing any source tag to depend on any tagged library. Frontend plugin configs (for example [`sales_ui`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/plugins/sales_ui/eslint.config.js)) spread the root config and add `nx.configs['flat/react']`. - `@typescript-eslint/no-explicit-any` is `off`; the ban on `any` comes from code review, not from lint. Do not read a green lint run as proof of type safety. - Prettier ([`.prettierrc`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.prettierrc)) enforces single quotes, trailing commas everywhere, and `endOfLine: "auto"`. The repository conventions add two-space indentation, kebab-case config files, PascalCase components, camelCase functions, and `UPPER_SNAKE_CASE` global constants; match the touched project where it differs. - Run `pnpm nx lint <project>` and `pnpm nx build <project>` before pushing; both must pass along with `pnpm nx test <project>` when the project defines tests. See [Testing](https://erxes.io/docs/contribute-testing). --- # Testing Project targets and manual flows How tests run in `erxes/erxes`, which Community Edition projects can run them, and what to check manually before a pull request. For the branch workflow, see [Contribute to codebase](https://erxes.io/docs/contribute-codebase). ## How Jest is set up - `jest.preset.js` is a one-line spread of the `@nx/jest` preset; each project's `jest.config.ts` applies it and sets `displayName`, transform (`babel-jest` with `@nx/react/babel` for UI projects, `ts-jest` where configured), and `coverageDirectory`. - The root `jest.config.ts` aggregates every project through `getJestProjectsAsync()`; it exists for IDE and whole-repo runs. - `nx.json` registers `@nx/jest/plugin` with `targetName: "test"`, so any project containing a `jest.config.*` file gets an inferred `test` target. `@nx/eslint/plugin` similarly infers `lint`, and `@nx/webpack/plugin` infers `build`/`serve`/`preview`. ## Project targets In the Community Edition, eight projects have a Jest config and therefore expose `pnpm nx test <project>`: `core-ui`, `erxes-ui`, `ui-modules`, `content_ui`, `frontline_ui`, `operation_ui`, `sales_ui` on the frontend, and `erxes-api-shared` on the backend, the only one that also declares an explicit `test` target in `project.json`. (Enterprise Edition plugin projects have their own configs.) Most backend plugins have no Jest setup at all: `sales_api`, `core-api`, and `content_api` have no `jest.config.ts`, so their spec files do not run under `pnpm nx test`. Always check `project.json` or run `pnpm nx show project <name>` instead of assuming a target exists. ```bash pnpm nx show project sales_ui # list the project's targets pnpm nx test erxes-api-shared # run one project's tests pnpm nx test core-ui pnpm nx affected -t test # tests for changed projects only pnpm nx affected -t lint,build,test # the full affected check before a PR ``` ## Where tests live (CE) Fifty-six spec/test files exist in the repo; these are the Community Edition ones: | Project | Path | Specs | | --- | --- | --- | | `erxes-api-shared` | `backend/erxes-api-shared/src/**` (`core-modules/`, `utils/`) | 27 | | `core-ui` | `frontend/core-ui/src/modules/**` | 5 | | `content_api` | `backend/plugins/content_api/src/modules/cms/postiz/__tests__/`, `test/` | 5 | | `content_ui` | `frontend/plugins/content_ui/src/modules/cms/**` | 4 | | `sales_api` | `backend/plugins/sales_api/src/modules/pos/` | 2 | | `saas-migrations` | `backend/saas-migrations/content/knowledgebase/__tests__/` | 2 | | `core-api` | `backend/core-api/src/modules/documents/__tests__/` | 1 | | `erxes-ui` | `frontend/libs/erxes-ui/src/modules/chat-viz/utils/` | 1 | The rest belong to Enterprise plugin projects. ## Representative specs Three projects cover most of the useful patterns: - **`erxes-api-shared`**: the deepest suite in the repo, and the only backend project with a `test` target set up end to end (`jest.config.ts` + explicit `project.json` target). The `core-modules/segments/` specs alone show how to test a pure engine: [`evaluate.test.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/segments/evaluate.test.ts) feeds segment definitions through the evaluator and asserts membership, with no MongoDB involved. Start here for backend logic. - **`core-ui`**: five specs over pure utility modules. `src/modules/navigation/utils/pinnedNavigationActivities.spec.ts` exercises the navigation helpers with plain inputs and outputs. The frontend pattern is the same: pick a pure function, feed it states, assert outputs. - **`sales_api`**: two specs (`modules/pos/fieldUtils.spec.ts`, `modules/pos/meta/automations/resolvers/resolvePosOrderPaymentUrl.spec.ts`) that exist without a project Jest config. They are dormant: readable as patterns, not runnable under `nx test` until the plugin adds a `jest.config.ts`. `content_api` is the fourth pattern and the exception: its Postiz specs run through [`jest.postiz.cjs`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/jest.postiz.cjs), a standalone `ts-jest` config that matches only `src/modules/cms/postiz/__tests__/*.spec.ts`, outside the Nx target system entirely. [`test/dockerfile.test.cjs`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/test/dockerfile.test.cjs) is a `node:test` contract test asserting the Dockerfile's `NODE_OPTIONS=--jitless` workaround stays in the installer stage. Its validation commands: ```bash pnpm nx build content_api node --test backend/plugins/content_api/test/dockerfile.test.cjs pnpm exec tsc --noEmit -p backend/plugins/content_api/tsconfig.json pnpm exec jest --config backend/plugins/content_api/jest.postiz.cjs --runInBand ``` `saas-migrations` uses yet another runner: `tsx --test` (`npm run test:knowledgebase`), the Node built-in runner rather than Jest. ## What to cover Co-locate specs under `__tests__/`. Backend service/model tests build tenant models with `generateModels('test')` and clean up in `afterEach`; frontend tests use `@testing-library/react` with Apollo's `MockedProvider`. Keep tests deterministic, isolated, and focused on behavior rather than implementation. Whatever the module, the priorities are the same: 1. **Permissions and tenant isolation**: a resolver or procedure that leaks another tenant's data is the worst regression; test the `subdomain` scoping, not just the happy path. 2. **Validation at the boundary**: bad input rejected before it reaches the model layer. 3. **The behavior you changed**: [`CONTRIBUTING.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CONTRIBUTING.md) asks for a test whenever the contribution adds observable behavior that is not already covered; a regression spec for a bug fix is worth more than a broad suite. ## Manual verification checklist Automated coverage is thin in most plugins, so `CONTRIBUTING.md` expects manual flows: 1. Reproduce the bug before fixing it, then confirm the same scenario passes. 2. Exercise every state your change touches: loading, success, empty, and error. 3. After each create, update, or delete, confirm the UI updates without a manual refresh (Apollo cache update, refetch, or `subscribeToMore`). 4. For widget and portal flows, test a fresh session, a reload, and a mobile viewport, and confirm no credentials leak into the URL or storage. 5. Re-run `pnpm nx lint <project>` and `pnpm nx build <project>` after review feedback, and record any check you could not run (with the reason) instead of describing it as passed. Attach screenshots or recordings to the PR for any visible UI change. --- # Releases Versions and image promotion How `erxes/erxes` versions are tagged, how Docker images and frontend bundles are published, and how to read a release as a self-hoster. For upgrading a deployment, see [Upgrades](https://erxes.io/docs/self-hosting-upgrades). ## Who cuts a release Maintainers create releases from the shared integration branches; contributors never update release metadata. Pull requests are squash-merged and release notes are generated, not hand-written. The release itself is driven by `release-it`: - `pnpm release` runs `release-it` (declared as a dev dependency, `^17.0.0`). - [`.release-it.json`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.release-it.json) disables npm publishing, commits with the message `release ${version}` (visible in history as `release 3.1.9`), creates a git tag named `${version}` matching the `3.*` pattern, and publishes a GitHub release named `v${version}`. - `@release-it/conventional-changelog` with the Angular preset rewrites `CHANGELOG.md` from conventional commit messages, which is why `feat(scope):` and `fix(scope):` matter. See [Contribute to codebase](https://erxes.io/docs/contribute-codebase). ## The changelog Each [`CHANGELOG.md`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/CHANGELOG.md) entry links the compare range, groups commits under `### Bug Fixes` and `### Features`, and keeps issue and commit links. The 3.1.9 entry looks like this: ```text ## [3.1.9](https://github.com/erxes/erxes/compare/3.1.8...3.1.9) (2026-09-17) ### Features * **core:** add "Basic information" system fields to Settings → Properties (c8b7214) * **core:** add member email activity log (#9350) (b65966c) * **frontline:** use "Visible to create" for convert properties on every kind (f93184b) ``` ## What CI publishes Per-area workflows under `.github/workflows/` run builds on pull requests and publish artifacts only on pushes to `main` or version tags: | Workflow files | Artifact | Tags | | --- | --- | --- | | [`ci-api-*.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/ci-api-core.yml) (gateway, core-api, 11 plugin APIs) | Docker image `erxes/erxes-next-<service>` (`erxes-next-gateway`, `erxes-next-core-api`, `erxes-next-sales_api`, …) | `latest` and `YYYYMMDD-<short-sha>` on `main` pushes (`core-api` also publishes from `debug/core`) | | [`ci-core-ui.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/ci-core-ui.yml) | `erxes/erxes-next-ui` | `latest`, branch, short SHA, and semver tags via `docker/metadata-action` | | `ci-service-*.yml`, `ci-saas-migrations.yml` | `erxes/erxes-next-logs`, `erxes/erxes-next-automations`, `erxes/erxes-next-migrations` | `latest` and date-SHA | | [`ci-apps-*.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/ci-apps-help-center.yml) | `erxes/frontline-widgets`, `erxes/posclient-front`, `erxes/help-center` | `latest` (`help-center` and `frontline-widgets` also get date-SHA or metadata tags) | | [`ci-ui-*.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/ci-ui-sales.yml) (plugin remotes) | No image; the Rspack Module Federation build is synced to Cloudflare R2 at `s3://erxes-next/<folder>/<name>_ui/` | Folder is `latest` on `main`, or the tag name on a version tag | ## The release workflow Pushing a tag matching `[0-9]*` (or running the workflow manually with a `version` input) triggers [`release.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/release.yml). For each of the 19 images in its matrix (the gateway, core-api, all plugin APIs, logs, automations, `erxes-next-ui`, `frontline-widgets`, `posclient-front`, and `help-center`), it runs: ```bash docker buildx imagetools create -t "<image>:<version>" "<image>:latest" ``` So a version tag does not rebuild anything; it promotes whatever `:latest` was current on Docker Hub at that moment. [`sentry-release.yml`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/sentry-release.yml) runs on the same tag and creates a Sentry release `erxes-<version>` on `sentry.erxes.io`, attaches commits, uploads source maps from `frontend/core-ui/dist` and `frontend/plugins/*/dist`, and marks a deployment. ## Reading a release as a self-hoster - Pin `erxes/erxes-next-<service>:3.1.9`-style tags rather than `latest`. The version tag is a copy of the `:latest` build that was current at release time, so the tag and `latest` can diverge immediately afterward. - A product version does not prove every image has a matching tag; check the set you deploy with `docker buildx imagetools inspect erxes/erxes-next-core-api:3.1.9` (or `docker manifest inspect`). - Read `CHANGELOG.md` for the fixes and features between your current version and the target, then check [Database Migrations](https://erxes.io/docs/self-hosting-migrations) before following [Upgrades](https://erxes.io/docs/self-hosting-upgrades). - Frontend plugin code is served from the versioned R2 folder, not from an image; a release updates both the images and the `erxes-next/<version>/` bundle prefix. --- # [Projects] Published 2026-10-13 > Operation — Manage Projects, Tasks, and Teams The Operation plugin helps you organize and track your team's work through projects, tasks, and teams. Projects The Projects section lets you create and manage projects. How **Operation — Manage Projects, Tasks, and Teams** The **Operation** plugin helps you organize and track your team's work through projects, tasks, and teams. ## **Projects** The **Projects** section lets you create and manage projects. ### How to open Projects 1. In the left sidebar, click **Plugins**. 2. Select **Operation**. 3. Click **Projects**. The Projects page shows all existing projects with columns for Name, Priority, Status, Tags, and Team. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b356befd-2e52-46f2-b219-d533a2fba500/public) ### Creating a new project 1. Click the **\+ Add project** button at the top right. 2. The **New project** panel opens. Fill in the project details: - **Project Name** - **Status:** Todo, In progress, Done, etc. - **Priority:** Low, Medium, High, etc. - **Assignee** - **Start Date** - **End Date** - **Teams** - **Tags** - **Description** You can also add rich text content in the description area using headings. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/81bcf766-fbc5-46ea-e8f4-8ff07094be00/public) 1. Click **Save**. The new project appears in the Projects list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f057cce1-88ef-444f-9ea6-ba7512477400/public) ### Project overview Click on a project name to open its overview page. The overview shows: - Project status, priority, assignee, and dates - Project description with headings - Project Report with progress stats: Total, Started, Completed - Tabs for Overview and Tasks - Notes section at the bottom ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c37e366f-af1f-4672-0a2f-d99ac914b500/public) ### Custom project properties You can add custom fields to projects through settings. 1. Go to **Settings → Properties → Operation Project Properties**. 2. The page shows core properties like Name, Status, Priority, Lead, Members, Teams, Tags, Start date, Target date, and Description. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/719c8f09-2590-47cc-ac2c-5b689dca4700/public) 1. Click **\+ Add field** to add a custom property. 2. Fill in: - Icon - Name - Code - Description - Group - Type - Show in card option - Logic rules ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cd11cca4-f7ed-45c3-4caf-64f285c2ec00/public) 1. Click **Add Property**. The custom property will appear on the project overview page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/052d39d0-b235-4155-a0cc-fac65c20f800/public) --- # Tourism — Complete User Guide Published 2026-10-13 > 1. Tourism Module Overview Path: Frontline → Tourism The Tourism module provides two systems for managing properties and tours: PMS (Property Management System): Manages property information, payments, user permissions, ## **1\. Tourism Module Overview** Path: Frontline → Tourism The Tourism module provides two systems for managing properties and tours: - PMS (Property Management System): Manages property information, payments, user permissions, appearance, and booking settings. - TMS (Tour Management System): Manages tour system information, user permissions, and payment settings. Select the required system from the Tourism sidebar. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f1468b7e-f603-4b6b-ee90-beb9b7e6be00/public) ## **2\. Property Management System (PMS)** Path: Frontline → Tourism → PMS PMS allows users to create and manage properties, such as hotels, and configure their information, payments, permissions, and booking settings. ### **2.1. PMS Overview** The PMS page displays created properties as cards. - Property Card: Displays the property name, logo or image, creation date, and assigned user. - Action: Opens available property actions. - Create PMS: Opens the property creation wizard. - Star: Adds the page to favorites. If no properties exist, an introductory card provides an option to create the first property. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7db38ca0-6255-4233-64e7-e74ce8f64300/public) ### **2.2. Creating a PMS** Path: Frontline → Tourism → PMS → Create PMS The creation wizard consists of five steps. Use Next and Previous to navigate between steps, or Cancel to exit. #### **Step 1: General Information** #### Enter the property details and configure check-in, check-out, discounts, and reservations. - Name: Property name (required). - Description: Brief property description. - Check-in / Check-out Time: Set guest arrival and departure times. - Check-in / Check-out Amount: Enter the associated amounts. - Add Discount: Configure discounts by product type, title, and pricing plan. - Website Reservation Lock: Enable or disable the website reservation lock. Click Next to continue. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b5915b7a-ddec-4731-712f-1cead7e54e00/public) #### **Step 2: Payments** Configure the payment methods available for the property. - Payments: Select an existing payment configuration. - Erxes App Token: Enter the token required for payment integration. - Add Payment Method: Add additional payment methods, such as golomtCard, khaanCard, or TDBCard. Click Next to continue. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/589daeac-a139-477d-b7c3-9c41331ede00/public) #### **Step 3: Permissions** Assign users to the appropriate property roles. - General Managers: Assign at least one general manager. - Managers: Assign users responsible for property management. - Reservation Managers: Assign users who manage reservations. - Reception: Assign reception staff. - Housekeeper: Assign housekeeping staff. Select the appropriate users from the dropdown fields, then click Next. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7768a321-3632-433c-f00e-6404169def00/public) #### **Step 4: Appearance** Customize the property's branding and website information. - Main Logo: Upload the property logo. - Primary: Set the primary brand color. - Secondary: Set the secondary color. - Third: Set an additional interface color. - Website: Enter the property's website URL. Click Next to continue. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/caa4170c-768e-44dd-4251-ea3a98b03a00/public) #### **Step 5: Pipeline Configuration** Connect the property to a sales pipeline and configure available products. - Board: Select the board. - Pipeline: Select the associated pipeline (required). - Room Categories: Select available room categories. - Exclude Room Categories / Rooms: Select categories or rooms to exclude. - Extra Product Categories: Select available additional product categories. - Exclude Extra Products: Select additional products to exclude. - Enable Appointments: Enable or disable appointment products. - Appointment Categories: Select available appointment categories. - Exclude Appointments: Select appointments to exclude. Review the settings and click Create to complete the process. A success notification appears when the property is created. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f6c683a7-e46f-48e6-e711-2e689f751700/public) ### **2.3. Viewing Created Properties** Created properties appear as cards on the PMS overview page. Each card displays the property name, logo or image, creation date, associated user, and available actions. Click Create PMS to add another property. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/dd8bc5eb-c913-4d53-ee38-403e686e5c00/public) ## **3\. Tour Management System (TMS)** Path: Frontline → Tourism → TMS TMS allows users to create and manage tour systems, including their general information, user permissions, and payment settings. ### **3.1. TMS Overview** The TMS page displays created tour management systems as cards. - TMS Card: Displays the system name, image or placeholder, creation date, and associated user. - Action: Opens available actions. - Create TMS: Opens the creation wizard. - Tourism Sidebar: Allows users to switch between PMS and TMS. If no image is uploaded, a placeholder is displayed. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d7ff94e8-c991-426f-ff05-3b8df87f1400/public) ### **3.2. Creating a TMS** Path: Frontline → Tourism → TMS → Create TMS The creation wizard consists of three steps. Use Next and Previous to navigate, or Cancel to exit. #### Step 1: General Information Configure the basic information and appearance of the tour management system. - Name: System name (required). - Main Color: Select the primary color (required). - Logo: Upload the main logo. - Fav Icon: Upload the favicon. - Language: Select the supported languages. Multiple languages are allowed. - Main Language: Select the default language. Logo and favicon images must be PNG files and no larger than 15 MB. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7a7e248d-b95b-44a4-3f96-26ecf3c72e00/public) #### Step 2: Permissions Assign users to manage the tour system. - General Managers: Assign at least one user responsible for general management. - Managers: Assign users responsible for managing the system. Assigned users may be displayed at the top of posts and in the list view. Click Next to continue. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8ebbb3f1-a7fb-495b-72ab-253c890c3e00/public) #### Step 3: Payments Configure the payment methods available in the tour management system. - Payments: Select an existing payment configuration. - Prepaid: Enable or disable the prepaid payment option. - Erxes App Token: Enter the token if required for payment integration. - Add Payment Method: Add additional payment methods. Each payment method includes: - Type: Payment method type. - Title: Display name. - Config: Payment configuration. - Delete: Remove the payment method. Examples include golomtCard, khaanCard, and TDBCard. Review the settings and click Save to complete the creation process. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7842534c-5c09-4c4d-c4ad-6adaa84d7900/public) ### 3.3. Viewing Created TMS After saving, the system returns to the TMS overview page. The newly created system appears as a card. Each card displays the system name, image or placeholder, available actions, creation date, and associated user. Click Create TMS to add another system. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/273c04d3-d31f-4628-12c7-7a722d368b00/public) ## 4\. Summary The Tourism module includes two systems: - PMS: Manages property information, payments, permissions, appearance, and pipeline settings. - TMS: Manages tour system information, permissions, and payments. - The PMS creation wizard has five steps, while the TMS wizard has three. - Both systems allow users to configure managers and payment methods. - Successfully created systems appear as cards on their respective overview pages. --- # [Forms] Published 2026-10-09 > Frontline Forms — Complete User Guide 1. Forms List Page Path: Frontline → Forms What it does The Forms section lets you create, manage, and publish data collection forms. Forms can be embedded on websites, shown as popu ## **Frontline Forms — Complete User Guide** ## **1\. Forms List Page** **Path:** Frontline → Forms ### What it does The Forms section lets you create, manage, and publish data collection forms. Forms can be embedded on websites, shown as popups, or used inside messenger. ### Forms list The list shows: - **Name** — The title of the form. - **Status** — Whether the form is active or inactive. - **Channel** — Which channel the form belongs to. - **Tags** — Labels attached to the form. - **Created By** — The user who created the form. - **Created At** — When the form was created. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/49a802c2-2145-437b-0278-74c720a40900/public) ## **2\. Create a New Form** Click the **\+ Create form** button to start building a form. ### Step 1: General Settings Fill in the basic form information: - **Title** — The form name shown to users. - **Description** — A short explanation of the form. - **Primary Color** — Choose the main brand color. - **Appearance** — Select how the form is displayed: - **Iframe** — Embed inside a webpage. - **Messenger** — Show inside the messenger widget. - **Load Type** — Choose how the form opens: - **Embedded** — Always visible on the page. - **Popup** — Opens when a user clicks a button. - **Button Text** — The text on the button that opens the form. - **Channel** — Assign the form to a channel. A live preview appears on the right side. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/04326d80-53a9-480d-5865-0bc4c34b1f00/public) ### Step 2: Callout Settings The callout is the invitation box that introduces the form. Options: - **Skip callout** — Toggle to disable the callout. - **Title** — Callout headline. - **Body** — Short message text. - **Button Text** — Text on the callout button. - **Image** — Upload an optional image. You can see the callout preview on the right. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/39690381-fa2c-48a3-9425-60f0fcc4f400/public) ### Step 3: Content This is where you build the form questions. #### Add a field Click **\+ Add Field** to choose a field type: - Text - Number - Checkbox - Date - Select - Countries - Textarea - Radio Group - Checkbox Group - Attachment - Customer fields ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/deba4d84-0ab2-417b-6b48-9daa9531aa00/public) #### Add multiple steps You can organize fields into steps: - Each step has a name and description. - Add fields inside each step. - Click **\+ Add Step** to create additional pages. - Use the preview on the right to test the flow. ### Step 4: Confirmation Settings This is the final message users see after submitting the form. Options: - **Title** — Confirmation headline. - **Description** — Thank-you or follow-up message. - **Image** — Upload an optional image. Click **Create form** to save and publish. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b68b9e9f-38a1-4b87-6e26-28c7a7047b00/public) ## **3\. Filter Forms by Channel** You can filter the forms list by channel. ### No forms in a channel If a channel has no forms, you see: - _"No forms found"_ - Buttons to **Create form** or **Go to channels**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c84d4d1a-f2c4-4600-b301-db43c0646d00/public) ### Forms in a selected channel When you select a channel that has forms, the list updates to show only forms from that channel. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2e000786-cc34-422b-a27d-721a8e674f00/public) --- # [Reports] Published 2026-10-09 > Frontline Reports — Complete User Guide 1. Reports Main Page Path: Frontline → Reports What it does The Reports section shows analytics and statistics about your Frontline activities. It helps you track conversations, ti ## **Frontline Reports — Complete User Guide** ## **1\. Reports Main Page** **Path:** Frontline → Reports ### What it does The Reports section shows analytics and statistics about your Frontline activities. It helps you track conversations, tickets, Facebook activity, and call center performance. ### Report tabs At the top, you can switch between different report categories: - **Frontline Overview** - **Ticket** - **Facebook** - **Call center** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d577488f-2c8b-466d-8236-3a54fd7b5a00/public) ## **2\. Frontline Overview Tab** This tab shows overall conversation statistics. ### Top summary cards - **Open Conversations** — Number of currently open conversations. - **Closed Conversations** — Number of closed conversations. - **Top Performing Source** — The channel with the most conversations. - **Top Converting Source** — The channel with the best conversion rate. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ffe9f89b-79cc-488e-c879-756490831b00/public) ### Conversation Open This chart shows the total number of conversations opened over the last 30 days. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/856e9a4a-fe44-4944-99ec-3c8c064f4800/public) ### Conversation Resolved This chart shows the total number of conversations resolved over the last 30 days. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/04b1d363-c793-413a-6f42-8564ccdb2d00/public) ### Conversation Source This chart shows where conversations are coming from, such as Messenger, Facebook, Email, etc. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cf0d21cf-982f-41a7-5a67-ebc664afdc00/public) ### Conversation Tag This chart shows which tags are used most often on conversations. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c6fdb43d-a10b-471c-3151-a97e164a4500/public) ### Conversation Responses This chart shows response activity over time. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/57e69c46-2bd0-4a5e-4151-a0976a004e00/public) ### Conversation List This chart shows a list of all conversations. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0cc6eda0-49db-4b74-f46f-aa80856c2d00/public) Each chart has a **Filter** button, a chart/table view toggle, a download button, and a fullscreen option. ## **3\. Ticket Tab** Click the **Ticket** tab to see ticket-related reports. ### Top summary cards - **Total Tickets** — Total number of tickets. - **Minor** — Tickets with low priority. - **Medium** — Tickets with medium priority. - **High** — Tickets with high priority. - **Critical** — Tickets with critical priority. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5631d44c-6a02-45df-9bbc-97303822dd00/public) ### Ticket Status Summary This table shows how many tickets exist in each status. Example: **New** status has 1 ticket, which is 100% of total. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bdeaa15c-8117-4807-d3cb-c98027725a00/public) ### Ticket Date This table shows tickets grouped by date. Example: On **2026-09-23**, there is 1 ticket. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7737929f-763c-47d2-f94f-01a9f1794b00/public) ### Ticket Source This chart shows where tickets are created from. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fd0bdf70-3c74-44b4-5e89-dff562932a00/public) ### Ticket Tags This chart shows which tags are used on tickets. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f05da141-26c1-4e45-0923-b2ae1099f900/public) ### Ticket Custom Properties This chart shows data from custom ticket fields. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/32f5b119-256f-4355-fe43-15bac6b94700/public) ### Ticket List This table shows all tickets with details: - Name - Created date - Status - State - Assigned person - Due date ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/481ccb76-7da1-4557-0780-6998ecd2f800/public) ## **4\. Facebook Tab** Click the **Facebook** tab to see Facebook-related reports. ### Top summary cards - **Messenger Conversations** — Total Facebook Messenger conversations. - **Messenger Messages** — Total messages, split between incoming and agent replies. - **Bot Messages** — Messages handled by bots. - **Posts** — Facebook posts tracked in erxes. - **Comments** — Comments and replies received. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cc0ad795-2f79-4119-29d4-5e60c5e08800/public) ### Facebook Activity This chart shows daily Facebook conversations, messages, and comments. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9a8f2a88-34ba-4d4b-9254-aaea20256d00/public) ### Facebook Bots This chart shows Messenger conversations handled by each bot. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/01d40157-d6ae-4868-e753-e7ed390b8b00/public) ### Facebook Posts This chart shows posts published in the selected period and their engagement. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/821ff9af-2a19-4b16-3414-d1def7f59f00/public) ## **5\. Call Center Tab** Click the **Call center** tab to see phone call reports. ### What it shows This tab is used when call integration is set up. You can filter by: - **Integrations** - **Queue** - **Direction** - **Date** — Example: Last 3 months If no call integration is connected, you see: - _"No call integration found for this user."_ --- # [Brands] Published 2026-10-09 > Brands — Filter Conversations by Brand The Brands section lets you organize and filter conversations by the brand they belong to. This is useful when your organization manages multiple brands or companies in one erxes ac **Brands — Filter Conversations by Brand** The **Brands** section lets you organize and filter conversations by the brand they belong to. This is useful when your organization manages multiple brands or companies in one erxes account. #### How to use Brands 1. Click **Frontline** in the left sidebar. 2. Open **Inbox**. 3. In the left panel, expand **Brands**. 4. Select a brand from the list, for example **nino**. 5. The conversation list will update to show only messages related to that brand. #### Why use Brands? - Separate conversations for different companies or product lines - Assign the right team members to each brand - Track performance per brand - Keep messaging and responses brand-specific > 💡 **Tip:** If you manage multiple brands, always check that the correct brand is selected before replying to a customer. --- # [Team inbox] Published 2026-10-09 > Team Inbox — Shared Team Channels The Team Inbox contains shared communication channels that your whole team can access. Each channel represents a connected source, such as a support email, social media page, or chat wid **Team Inbox — Shared Team Channels** The **Team Inbox** contains shared communication channels that your whole team can access. Each channel represents a connected source, such as a support email, social media page, or chat widget. #### How to open Team Inbox 1. Click **Frontline** in the left sidebar. 2. Open **Inbox**. 3. In the left panel, expand **Team inbox**. 4. Select a channel, for example **Test Channel**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0f2c3843-f833-45c9-916f-a9d07fc4e700/public) #### What you can do in Team Inbox - View all team conversations in one place - Assign messages to yourself or other team members - Reply to customers together with your team - Filter by channel, status, or response time > 💡 **Tip:** Make sure unassigned messages in the Team Inbox are picked up quickly to avoid delays. --- # [My inbox] Published 2026-10-09 > My Inbox — Your Personal Inbox My Inbox contains your private and direct conversations. Under this section, you will find the Personal inbox , which stores messages that are only visible and accessible to you. How to ope **My Inbox — Your Personal Inbox** **My Inbox** contains your private and direct conversations. Under this section, you will find the **Personal inbox**, which stores messages that are only visible and accessible to you. #### How to open My Inbox 1. Click **Frontline** in the left sidebar. 2. Open **Inbox**. 3. In the left panel, expand **My inbox**. 4. Click **Personal inbox**. #### When to use Personal inbox - For private messages that are not shared with the team - For direct customer communication assigned only to you - To separate your personal workload from the team queue > 💡 **Tip:** If your Personal inbox is empty, it means no private conversations have been assigned to you yet. --- # [Work Queue] Published 2026-10-09 > Work Queue — Manage Your Assigned Tasks The Work Queue helps you keep track of conversations that need your attention. It is divided into four main views: Assigned To Me , Mentions , Unassigned , and Awaiting Response . **Work Queue — Manage Your Assigned Tasks** The **Work Queue** helps you keep track of conversations that need your attention. It is divided into four main views: **Assigned To Me**, **Mentions**, **Unassigned**, and **Awaiting Response**. #### How to open Work Queue 1. In the left sidebar, click **Plugins**. 2. Select **Frontline**. 3. Click **Inbox**. 4. In the left panel, expand the **Work queue** section. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/180b1616-6c04-42f1-2d41-e973f373e700/public) #### Work Queue views **Assigned To Me** Shows all conversations that are assigned directly to you. This is your personal to-do list within the team inbox. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/dd7a1700-227f-4fd2-99e2-49e6f801c900/public) **Mentions** Displays conversations where someone has mentioned you using @. Use this view to quickly find messages that require your input. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/28f91de0-a0c6-4266-73b8-2573afb65800/public) **Unassigned** Lists conversations that have not been assigned to any team member yet. Managers can use this view to distribute work. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/08a402ef-415c-4f21-96fd-526e744eda00/public) **Awaiting Response** Shows conversations where a reply is expected. This helps you follow up on pending customer or team messages. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2da622e1-f185-43c5-f591-38377a956300/public) #### How to filter conversations 1. Click the filter icon at the top of the conversation list. 2. Choose from the available filters such as Unresolved, Resolved, Unassigned, Participated, Awaiting response, Automation status, By Channel, Integration Type, or Created at. 3. The conversation list will update automatically. > 💡 **Tip:** Use the **Awaiting Response** view daily to make sure no message is left unanswered. --- # [My Tasks] Published 2026-10-09 > Operation — Manage Projects, Tasks, and Teams The Operation plugin helps you organize and track your team's work through projects, tasks, and teams. Tasks The Tasks section helps you track individual work items. How to o **Operation — Manage Projects, Tasks, and Teams** The **Operation** plugin helps you organize and track your team's work through projects, tasks, and teams. ## **Tasks** The **Tasks** section helps you track individual work items. ### How to open Tasks 1. In the left sidebar, click **Plugins**. 2. Select **Operation**. 3. Click **My Tasks**. The Tasks page has tabs for: - **Assigned:** Tasks assigned to you - **Created:** Tasks you created ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8fd2bbda-32c4-4a13-f262-18fc91175a00/public) ### Creating a new task 1. Click the **\+ Add task** button. 2. The **New task** panel opens. Fill in the task details: - **Task Name** - **Status** - **Priority** - **Assignee** - **Project** - **Milestone** - **Estimate** (points) - **Start Date** - **Target Date** - **Tags** - **Description** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b03eb039-e44e-4e5f-2e3b-512bebf02100/public) 1. Click **Save**. The task appears in your task list. The right sidebar shows the Task Report with progress and priority breakdown. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/76c397e0-de47-435f-198a-0e8c25286000/public) ### Creating a task for a team When creating a task, you can assign it to a specific team. 1. In the New task panel, select the team from the dropdown at the top. 2. Fill in the task details. 3. Click **Save**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/afa49754-7151-4644-970a-fa5aa2087b00/public) ## **Teams** The **Teams** section lets you create and manage operation teams. ### How to open Teams settings 1. Go to **Settings → Operation → Teams**. If no teams exist, you will see the **No team yet** empty state. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/18288972-5665-484a-0340-3c1753716000/public) ### Creating a new team 1. Click the **\+ Create team** button. 2. The **Add team** panel opens. Fill in: - **Icon** - **Team Name** - **Description** - **Members** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/75e39cbf-c712-4ba5-1eef-4e1a807ffb00/public) 1. Click **Create**. The team details page opens. Here you can: - Edit the team name and description - Manage team members - Set estimate options - Configure task statuses - Set team cycles ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/30dd0d7e-b505-4f6a-0d0d-5629fef61000/public) ### Team projects and tasks After creating a team, it appears in the left sidebar under **Your Teams**. Click on the team to access: - **Projects:** View projects assigned to this team ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/567cb0a1-a61a-4b05-e38b-1fcc98936500/public) - **Tasks:** View tasks assigned to this team ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6246b5af-b380-4564-0152-de1a594aa200/public) --- # [Tickets] Published 2026-10-09 > Frontline Tickets — Complete User Guide 1. Tickets List Page Path: Frontline → Team Inbox → Tickets What it does The Tickets page shows all support tickets created in your Frontline system. Tickets help your team track c ## **Frontline Tickets — Complete User Guide** ## **1\. Tickets List Page** **Path:** Frontline → Team Inbox → Tickets ### What it does The Tickets page shows all support tickets created in your Frontline system. Tickets help your team track customer issues, questions, or tasks from start to finish. ### What is on the page At the top: - **1 record found** — Shows how many tickets exist. - **Import / Export** — Buttons to import tickets from a file or export the list. - **View** — Change how the list is displayed. - **Sort** — Sort tickets by different criteria. - **\+ Add ticket** — Button to create a new ticket. The table columns: - **NAME** — The title of the ticket. - **STATUS** — The current stage of the ticket. Example: **New**. - **CHANNEL** — Which channel the ticket belongs to. Example: **Test Channel**. - **PIPELINE** — Which pipeline the ticket is in. Example: **123**. - **BRANCH** — The branch related to the ticket. Example: **Broadway Wintergarden**. - **DEPARTMENT** — The department related to the ticket. - **ASSIGNEE** — The person assigned to the ticket. In the left sidebar under **Frontline → Channels**, you can filter tickets by: - Channel name. Example: **Test Channel** - Pipeline name. Example: **Test Pipeline** - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/deb92094-083d-4f34-eaa9-9c09036a4900/public) ## **2\. How to Add a New Ticket** 1. Click the **\+ Add ticket** button at the top right. 2. A **New ticket** panel opens on the right side. ### Fields to fill: - **Ticket Name** — Enter the title of the ticket. - **Channel** — Select which channel this ticket belongs to. Example: **Test Channel** - **Pipeline** — Select the pipeline. Example: **123** - **Status** — Choose the ticket status. Example: **New** - **Priority** — Choose priority level. Example: **No Priority** - **Assignee** — Select a team member to handle the ticket. - **Branch** — Select the related branch. - **Department** — Select the related department. - **Start Date / Due Date** — Set when the ticket starts and when it is due. - **Tag** — Add tags to organize the ticket. 1. Click **Save** to create the ticket. 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/114f5cca-b33b-4cf2-b26d-965bc456b500/public) ## **3\. Ticket Detail Page** **Path:** Click on any ticket name from the Tickets list. ### What it shows The Ticket Detail page has all the information about one ticket. At the top: - **Ticket title** — Example: **Test** - **Channel** — Example: **Test Channel** - **Pipeline** — Example: **123** - **Branch** — Example: **Broadway Wintergarden** - **Department** — Example: **Broadway** - **Status** — Example: **New** - **Priority** — Example: **Medium** - **Creator / Assignee** — Example: **broadway temuulen** Below the title: - **Select assigned members** — Assign more team members. - **Start Date / Due Date** — Example: **Sep 23 – Sep 25** - **Select Tag** — Add or change tags. - **Archive** — Move the ticket to archive. - **UnSubscribe** — Stop receiving updates about this ticket. - **Add attachments** — Upload files related to the ticket. In the middle: - The ticket description or conversation. Example: hi this one is test for video - A note section at the bottom where you can type: Leave a note... At the bottom: - **Created by broadway temuulen — about 1 hour ago** — Shows who made the ticket and when. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/05a90fe2-94ab-4d9b-3bc7-5bfe18fc1f00/public) ## **4\. Ticket Detail Tabs** On the left side of the Ticket Detail page, there are two tabs: ### Overview This is the main view. It shows the ticket title, details, description, and activity history. ### Properties This tab shows extra custom fields and connected information. In the example, it includes sections like: - **STRIPE PAYMENT** — Fields for payment-related information. - **ERXES TICKETS** — Custom fields such as: - Language - Plugin - How can we understand your request? - Issue resolution - Weight of ticket These fields are configured by your admin and may be different for each organization. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bb6f204e-d332-4a73-343f-89b1c4b8f900/public) ## **5\. Right Sidebar in Ticket Detail** On the right side of the Ticket Detail page, there are several icons. Each icon opens a different related section. ### Customers - Shows customers connected to this ticket. - If empty: **No customers to display at the moment.** - You can click **\+ Add Customer** to connect a customer. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a9f81333-3b19-4ede-f1f5-3eba9aec9e00/public) ### Companies - Shows companies connected to this ticket. - If empty: **No companies to display at the moment.** - You can click **\+ Add Company** to connect a company. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/15f60bdc-5941-446f-1401-17672dd97e00/public) ### Progress - Shows ticket progress statistics. - Example: - New: 0 - Open: 0 - Closed: 0 - Resolved: 0 - You can switch between **All Time** and other time ranges. - Below the stats, you can filter by **Assignee**, **Source**, or **Tag**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/206ba3c2-9ca3-4009-410b-9c88f50ca100/public) ### Transactions - Shows payment or transaction records related to the ticket. - If empty: **No transactions to display.** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a4360a74-f804-4fec-0191-200aab496000/public) ### Score Summary - Shows loyalty or reward points related to the customer. - Example: - Total Point Earned: 0 - Points Balance: 0 - Total Point Redeemed: 0 - Also shows: - Redemption Rates: 0.0% - Active Loyalty Members: 0 - Monthly Active Users: 0 - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/492bc16c-82ce-40e8-73f0-bd604d316e00/public) ### Deals - Shows sales deals connected to this ticket. - If empty: **No deals to display at the moment.** - You can click **\+ Add a deal** or **Choose an existing deal**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/043191e2-cb7c-4ed0-e808-2360dacc5c00/public) ### POS Orders - Shows point-of-sale orders related to the ticket. - If empty: **No POS orders to display at the moment.** - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cd2411db-8881-4cd5-5485-a07996582100/public) ### Invoices - Shows invoices connected to the ticket. - If empty: **No invoices to display at the moment.** - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9b3aecbd-07db-4613-bd95-30069c4c0d00/public) ### Triages - Shows triage records, which are used for sorting or categorizing tickets. - If empty: **No triages to display at the moment.** - You can click **\+ Add triage** to create one. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/08eb1ccf-172e-4fc4-8db8-ea9b57cfa300/public) ### Projects - Shows projects connected to this ticket. - If empty: **No projects to display at the moment.** - You can click **\+ Add project** to connect a project. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/532cdcd1-56c7-4390-a508-5f1243eec600/public) ## **6\. Empty Tickets Page** **Path:** Frontline → Tickets (when no tickets exist) ### What it shows - **0 records found** - **No tickets found** - The list is empty because no tickets have been created yet or the selected filter shows no results. To create a ticket, click **\+ Add ticket**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/357d25b9-4a2c-48c6-c6b2-f889f7022500/public) --- # [Surveys] Published 2026-10-09 > Surveys — Create and Manage Customer Feedback Surveys The Surveys section in Frontline lets you create, send, and track feedback surveys. Surveys help you collect customer opinions, measure satisfaction, and improve your **Surveys — Create and Manage Customer Feedback Surveys** The **Surveys** section in Frontline lets you create, send, and track feedback surveys. Surveys help you collect customer opinions, measure satisfaction, and improve your service. #### How to open Surveys 1. In the left sidebar, click **Plugins**. 2. Select **Frontline**. 3. Click **Surveys**. 4. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1d7d6f69-da32-41c6-a633-3740473aad00/public) The survey list shows each survey card with: - **Name:** Survey title - **Question:** Main question text - **Status:** Active or inactive - **Options:** Answer choices with vote counts and percentages - **Total votes, voters, and sends** When you have more than one survey, they will appear side by side in the list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/912aabbc-a4f2-43a4-c4ea-2b50d9856300/public) #### How to manage surveys through channel settings Surveys are connected to channels. To manage surveys for a specific channel: 1. Click **Settings** in the left sidebar. 2. Go to **Channels** under the Frontline section. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/63779d1f-07d5-4416-1ae4-a7eff0ff8f00/public) 4. Click the channel you want to manage, for example **Test Channel**. 5. Scroll down and click **Manage channel surveys**. 6. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a87ff8b6-007a-4aef-4253-06476add5d00/public) This will open the surveys list for that channel. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e3cd6a11-a9c4-443d-adc3-d3bf350b5e00/public) #### How to create a new survey 1. In the Surveys view, click the **\+ Create survey** button at the top right. 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a47b6e6b-4ac2-4cf5-b801-aacda36b5c00/public) 3. The survey builder opens with three steps. Step 1: General Fill in the basic survey information: - **Title:** Enter the survey name - **Brand:** Select the brand this survey belongs to The right side shows a live preview of how the survey will look on desktop, tablet, and mobile. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0b7f9c0c-0de8-42d1-1c2c-1080195ea300/public) Click **Next step** to continue. Step 2: Content Add your survey questions and answer options. - **Steps:** Add a title for this step - **Question:** Type the question you want to ask - **Attachments:** Upload an attachment if needed - **Options:** Add answer choices You can add multiple options for respondents to choose from. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/47976b79-8bb1-4d1a-1a69-a88a4886a400/public) The preview on the right updates automatically as you build your survey. Click **Next step** when finished. Step 3: Confirmation Set the final survey settings: - **Duration:** Choose how long the survey will be active Review your survey in the preview panel. The confirmation screen also shows a summary of the steps and questions. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/54836aa0-1298-42fe-00b9-70956ec92500/public) Click **Create survey** to publish it. #### How to read survey results Once surveys are active and responses come in, the survey cards will display: - Votes per option - Percentage for each option - Total votes, voters, and sends - > 💡 **Tip:** Keep your surveys short and clear to get more responses. One main question per survey usually works best. --- # [POS] Published 2026-10-09 > Sales — Manage Deals, Pipelines, and POS Orders The Sales plugin helps you manage your sales process, from deal tracking to point-of-sale orders. It has two main areas: Sales Pipeline and POS . POS (Point of Sale) The PO **Sales — Manage Deals, Pipelines, and POS Orders** The **Sales** plugin helps you manage your sales process, from deal tracking to point-of-sale orders. It has two main areas: **Sales Pipeline** and **POS**. ## **POS (Point of Sale)** The **POS** section lets you view and manage point-of-sale orders and related reports. ### How to open POS 1. In the left sidebar, click **Plugins**. 2. Select **Sales**. 3. Click **POS**. The POS menu includes: - POS Orders - POS Covers - POS By Items - POS Items - POS Summary - POS Orders By Customer ### POS Orders Click **POS Orders** to view all completed POS orders. The list shows: - Bill Number - Date - Cash Amount - Mobile Amount - Card - QPay - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2caf5a16-2e8d-484b-327e-4b8d4c7df500/public) ### POS Covers Click **POS Covers** to view cover information. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8b857eda-1b46-464a-b1f4-9977c4b85c00/public) ### POS By Items Click **POS By Items** to see sales broken down by individual items. The list shows: - Code - Name - Category - Unit Price - Quantity sold per time slot - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1e6d7154-7ce2-42c9-f898-44b52e2d4900/public) ### POS Items Click **POS Items** to view all POS item transactions. The list shows: - Number - Created Date - Created Time - POS - Branch - Department - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/61b8889b-78c5-46a5-aa29-45c2d4ae4e00/public) ### POS Summary Click **POS Summary** to see a summary of POS sales. The summary shows: - Count - Cash Amount - Mobile Amount - Card Amount - Invoice - QPay Amount - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0be796c8-0f3d-4b50-9714-7a65876aa200/public) ### POS Orders By Customer Click **POS Orders By Customer** to view orders grouped by customer. --- # [Sales Pipeline] Published 2026-10-09 > Sales — Manage Deals, Pipelines, and POS Orders The Sales plugin helps you manage your sales process, from deal tracking to point-of-sale orders. It has two main areas: Sales Pipeline and POS . Sales Pipeline The Sales P **Sales — Manage Deals, Pipelines, and POS Orders** The **Sales** plugin helps you manage your sales process, from deal tracking to point-of-sale orders. It has two main areas: **Sales Pipeline** and **POS**. ## **Sales Pipeline** The Sales Pipeline section lets you create boards and pipelines to track your deals through different stages. ### How to open Sales Pipeline settings 1. In the left sidebar, click **Plugins**. 2. Select **Sales**. 3. Click **Sales Pipeline**. You can also go to **Settings → Sales → Deals**. ### Boards and Pipelines The **Boards & Pipelines** page shows all your sales boards. Each board can contain multiple pipelines. - **Boards:** Groups of pipelines - **Pipelines:** Specific workflows for deals - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/78a548c2-d32c-443f-c7be-e5d3d987f600/public) To create a new board, click the **+** icon next to **Boards**. To create a new pipeline, click the **\+ Add Pipeline** button at the top right. ### Creating a new pipeline 1. Click **\+ Add Pipeline**. 2. The **Add Pipeline** panel opens with four tabs: **General**, **Stages**, **Product Config**, and **Properties**. #### General tab Fill in the basic pipeline settings: - **Name:** Pipeline name - **Visibility:** Public or Private - **Board:** Select the board this pipeline belongs to - **Tag:** Add tags - **Branches:** Select related branches - **Departments:** Select related departments - **Number Configuration:** Set the starting number and attribution - **Name Configuration:** Configure how deal names are generated - **Users eligible to see all deals:** Choose users who can view all deals - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0e878352-b91c-426a-546e-3ddbb7fc0e00/public) #### Stages tab Click the **Stages** tab to add and manage pipeline stages. 1. If no stages exist, click **\+ Add Stage**. 2. For each stage, fill in: - **Stage Name** - **Probability** - **Status** - **Visibility** - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0169484a-4f9e-4ed5-82de-e2456910d700/public) You can add multiple stages to represent your sales process. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cb7908c7-3432-4ce7-44d2-20ea4c9e6a00/public) #### Product Config tab Click the **Product Config** tab to configure product-related settings. - **Initial Product Categories:** Choose default product categories - **Pipeline Exclude Products:** Exclude certain categories or products - **Payments:** Select payment methods - **Other Payment:** Add custom payment types - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/baac9b17-d2f3-43aa-f443-3ac6114c3400/public) You can add custom payment methods with type, title, icon, config, and score campaign. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/942c158e-b046-47f4-ae59-a51ad62a7200/public) #### Properties tab Click the **Properties** tab to configure custom properties and relations for deals in this pipeline. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fa94c58f-075e-4d4d-95a4-1996c4ea9400/public) ### Pipeline created After filling in all tabs, click **Create**. The new pipeline will appear in the Boards & Pipelines list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/13491d56-06ae-4ac9-b19a-a2748b72f100/public) ### Using the Sales Pipeline Once your pipeline is ready, go to **Sales → Sales Pipeline** to view the board. Deals are displayed as cards in columns based on their stage. You can drag and drop cards between stages. --- # [Knowledge Base] Published 2026-10-09 > Knowledge Base — Build Help Articles and Guides The Knowledge Base in Frontline helps you create and organize help articles for your team and customers. Content is structured in three levels: Topics , Categories , and Ar **Knowledge Base — Build Help Articles and Guides** The **Knowledge Base** in Frontline helps you create and organize help articles for your team and customers. Content is structured in three levels: **Topics**, **Categories**, and **Articles**. #### How to open Knowledge Base 1. In the left sidebar, click **Plugins**. 2. Select **Frontline**. 3. Click **Knowledge Base**. 4. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ebc6934a-f75a-4dd3-3284-f1c559b14b00/public) The left panel shows the structure: - **Topics:** Top-level sections - **Categories:** Groups of articles inside a topic The main area displays topic cards with their descriptions and number of categories. #### Structure of Knowledge Base Knowledge Base uses a simple hierarchy: 1. **Topic** — A broad subject area 2. **Category** — A group of related articles under a topic 3. **Article** — The actual help content #### How to create a new topic 1. In the Knowledge Base view, click the **\+ New Topic** button at the top right. 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f6b16fa5-2508-4984-26de-d97ea5ef4000/public) 3. The **New Topic** panel opens. Fill in the details: **General tab:** - **Title:** Enter the topic name - **Description:** Add a short description - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/eef63f19-9739-49a9-4b0d-401e34f7cc00/public) **Appearance tab:** - **Color:** Choose a color for the topic - **Background Image:** Upload an image if needed - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bca6f91b-d223-49b9-e9ae-95f87b40f800/public) 1. Click **Create Topic**. The new topic will appear in the Knowledge Base list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f40e6e68-9154-4838-21f2-dc0aa95fc500/public) #### How to create a category 1. Open a topic that does not have categories yet. 2. Click the **Create Category** button. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d660848a-f65d-411f-202b-bacc544e1c00/public) 4. Fill in the category details: - **Code:** Optional reference code - **Title:** Category name - **Description:** Short explanation - **Icon:** Choose an icon - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4947bd38-a1c9-4ad2-f03c-fb524cbfb700/public) 1. Click **Create Category**. The category will appear inside the topic. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ad7c6c8b-276f-4dbe-dfe9-fd64b04e1300/public) #### How to create an article 1. Open a category. 2. If the category is empty, click the **Create** button. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/658ae760-639a-4ff9-5b17-af5f8082e500/public) 4. The **New Article** panel opens. Fill in the article details: - **Title:** Article headline - **Summary:** Short description - **Content:** Main article text - **Status:** Draft, Published, or Archived - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e1c44770-de31-4768-f5fb-4b2bc8557f00/public) 1. Click **Save**. The article will appear in the category list with its status. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cb6f7600-362e-4ed3-dbe1-6ea63997cc00/public) #### How to edit an article 1. In the article list, click on the article you want to edit. 2. The **Edit Article** panel opens. You can update: - **Title** - **Summary** - **Content** - **Status** - **Privacy settings** - **Reaction choices** - **Media:** Upload an image - **Attachments:** Add files or PDFs - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/569bb9da-323a-4337-c5a7-290b460fda00/public) **Publishing settings:** - Change the status between Draft and Published - Set whether the article is private - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d4ec08f4-9176-40b7-e2d1-c1969c0f2800/public) **File details:** - Add file URL, size, name, and type for attachments - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/56d405d5-af00-4acd-07e4-f227a2d42700/public) 1. Click **Save** when done. #### How to manage topics You can manage a topic by clicking the three-dot menu next to its name in the left panel. Options include: - **Edit Topic** - **View Script** - **Add Category** - **Delete Topic** - > 💡 **Tip:** Organize your Knowledge Base with clear topic and category names so users can find answers quickly. --- # [Inbox] Published 2026-10-09 > Frontline Inbox — Complete User Guide 1. Inbox Main Page Path: Frontline → Inbox What it does The Inbox is the main place where all customer conversations come together. It shows messages from all connected channels like ## **Frontline Inbox — Complete User Guide** ## **1\. Inbox Main Page** **Path:** Frontline → Inbox ### What it does The Inbox is the main place where all customer conversations come together. It shows messages from all connected channels like Messenger, Facebook, Instagram, Email, and more. ### What is on the page At the top: - **Inbox** title. - **0 conversations** — Shows how many conversations are currently displayed. - Filter and view options. In the left sidebar under **Frontline**, you can switch between: - **Inbox** - **Tickets** - **Reports** - **Forms** - **Surveys** - **Knowledge Base** - **Help Center** Below that, there is a **Work queue** section with quick filters: - **Assigned To Me** - **Mentions** - **Unassigned** - **Awaiting Response** If the inbox is empty, you see: - **No conversations found** - _"There are no conversations matching your filters."_ - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/339eaae7-8d04-478a-122e-f467423b5300/public) ## **2\. How to Use the Filter** At the top of the inbox, there is a filter button. Click it to open the filter menu. The filter options include: - **Search** — Search for specific conversations. - **Unresolved** — Show only open conversations. - **Resolved** — Show only finished conversations. - **Unassigned** — Show conversations with no assignee. - **Participated** — Show conversations you joined. - **Awaiting response** — Show conversations waiting for a reply. - **Automation status** — Filter by automation rules. - **By Channel** — Filter by specific channel. - **Integration Type** — Filter by Messenger, Facebook, Email, etc. - **Created at** — Filter by date. You can combine multiple filters at the same time. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ccac4143-6359-4cb6-2fb8-2ebfc39ae100/public) ## **3\. Assigned To Me Filter** Click **Assigned To Me** in the left sidebar to see only conversations assigned to you. At the top, a filter tag appears: **Assigned to me**. If no conversations are assigned to you, you see: - **No conversations found** - _"There are no conversations matching your filters."_ - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0c5b8314-dad7-48b2-7240-32da8d483500/public) ## **4\. Mentions Filter** Click **Mentions** in the left sidebar to see conversations where someone mentioned you. At the top, a filter tag appears: **Mentions**. If no one mentioned you, the inbox will be empty. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0c99996a-4a09-4ce9-da0d-ae3696d21300/public) ## **5\. Unassigned Filter** Click **Unassigned** in the left sidebar to see conversations that do not have an assignee yet. At the top, a filter tag appears: **Unassigned**. This is useful for finding new customer messages that need to be assigned to a team member. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/464f6668-c8d3-4a45-f8ad-905e060c9700/public) ## **6\. Awaiting Response Filter** Click **Awaiting Response** in the left sidebar to see conversations where a customer is waiting for your reply. At the top, a filter tag appears: **Awaiting response**. This helps your team prioritize messages that need attention. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/170efc49-287e-44e0-52b6-e03f914aef00/public) ## **7\. Combining Filters** You can use more than one filter at the same time. For example: - **Awaiting response** + **Personal inbox** The filter tags will both appear at the top. You can remove a filter by clicking the **X** on the tag. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/52c9cbbc-b76b-46fd-5643-967c381ba700/public) Another example: - **Awaiting response** + **Test Channel** This shows only conversations in the Test Channel that are waiting for a response. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/350caddc-462c-4741-3ca8-7b2997c56800/public) ## **8\. Filter by Brand** You can also filter conversations by brand. In the left sidebar under **Brands**, select a brand name. Example: **nino**. At the top, a filter tag appears with the brand name. This is useful if your company manages multiple brands in one system. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b504fa85-4780-4d48-1a2b-926a78b1f800/public) --- # [Help Center] Published 2026-10-09 > Help Center — Create a Public Support Portal The Help Center lets you build a public support portal for your customers. You can connect your Knowledge Base articles, ticket forms, custom forms, and CMS content in one pla **Help Center — Create a Public Support Portal** The **Help Center** lets you build a public support portal for your customers. You can connect your Knowledge Base articles, ticket forms, custom forms, and CMS content in one place. #### How to open Help Center 1. In the left sidebar, click **Plugins**. 2. Select **Frontline**. 3. Click **Help Center**. 4. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ba18b453-a7cc-448d-33a8-14385d66a900/public) The list shows existing help centers with: - **Name:** Help center title - **Client Portal:** Linked client portal - **Knowledge Base Topic:** Connected knowledge base topic - **Ticket Channel:** Linked ticket channel When you create more than one help center, they will appear in the list together. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d3fb8279-1b3d-4044-3421-2578f2466100/public) #### How to create a new help center 1. Click the **\+ Create Help Center** button at the top right. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b4fb8308-f270-4b83-3819-500cd407cc00/public) 1. The **New Help Center** panel opens with two tabs: **General** and **Appearance**. General settings Fill in the basic information: - **Name:** Help center title - **Client Portal:** Select the client portal - **Description:** Add a description - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/57b15322-a22d-4456-806c-36cd45ed8500/public) **Knowledge Base section** - Enable **Show Knowledge Base** to display articles on the published site. - **Knowledge Base Name:** The name shown on the menu - **Knowledge Base Topic:** Select the topic to display - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f9856185-9b2d-47cd-da85-0bde6ed94400/public) **Tickets section** - Enable **Show Tickets** to let visitors raise tickets. - **Ticket Name:** The name shown on the menu - **Channel:** Select the ticket channel - **Pipeline:** Select the ticket pipeline - **Status:** Select the default ticket status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9c878fa4-6185-44fe-2663-070a7ab99700/public) **Forms section** - **Channel:** Select the channel - **Forms:** Choose which forms to display - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9d545a74-a178-4ff9-8f28-429cecc09800/public) **CMS section** - **CMS:** Select a CMS source if you want to include CMS content - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/238aafbb-62b9-4148-ae6a-cb19d7ce1400/public) Appearance settings Click the **Appearance** tab to customize the look of your help center. **Logo and Favicon** - Upload a **Main Logo** - Upload a **Favicon** - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/924bfb45-5267-480b-bc24-bf67169c6400/public) **Main Colors** Customize colors for: - Body - Header - Footer - Help Center - Background - Active Tab **Fonts and Color** - Base Font - Heading Font - Base Color - Heading Color - Link Text - Link Hover Text **Form Elements Color** - Primary Action Button - Secondary Action Button - Heading Divider & Input Focus Glow **Topic Appearance** - Color - Background Image - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bb4fc860-3774-4b98-958e-cd07ec91b800/public) **Header** Customize header labels: - Wordmark - Home Tab - Forms Tab - Announcements Tab - Search Placeholder - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a772d784-3a00-4894-07f1-f7d02ca19900/public) **Footer** Customize: - Footer Logo - Footer Description - Copyright Line - Footer Link Columns - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9c65d39b-0f37-47ed-6f09-18f0db231700/public) **Advanced** For custom designs, you can add: - Header HTML - Footer HTML - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6576275b-08f6-4536-cc5b-d7cd1b797200/public) #### Finalizing the help center After filling in both the General and Appearance tabs, click **Create Help Center** at the bottom right. Once created, your help center will appear in the list. > 💡 **Tip:** Choose a clean color scheme and upload your brand logo to make the help center look professional and consistent with your company identity. --- # [MSDynamic] Published 2026-10-09 > Mongolian Plugin — Manage Local Integrations and Syncs The Mongolian plugin contains tools for managing local Mongolian service integrations. It includes three main sections: Put Response , Erkhet Sync , and MSDynamic . **Mongolian Plugin — Manage Local Integrations and Syncs** The **Mongolian** plugin contains tools for managing local Mongolian service integrations. It includes three main sections: **Put Response**, **Erkhet Sync**, and **MSDynamic**. #### How to open the Mongolian plugin 1. In the left sidebar, click **Plugins**. 2. Select **Mongolian**. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6fc9bb71-e7bb-4c88-4117-7b624927c300/public) The Mongolian plugin menu shows: - **Put Response** - **Erkhet Sync** - **MSDynamic** **MSDynamic**   - The **MSDynamic** section is used to synchronize data with Microsoft Dynamics. - #### Sync History - Click **MSDynamic** in the Mongolian menu. - Select **Sync History**. - The Sync History page shows the same type of log information as Erkhet Sync. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/19f08f9d-776a-43f5-5787-8605602ca000/public) - #### Check Orders - Click **Check Orders** to review order records synced from MSDynamic. - The list shows: - Created At - Number - Total Amount - Paid At - Synced Date - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f889e60a-47f1-4715-aee4-ff2aca3f5b00/public) - #### Check Categories - Click **Check Categories** to review category records. - At the top, you can: - **Create Categories** - **Update Categories** - **Delete Categories** - Select a category - Select a brand - The list shows: - Code - Name - Description - Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5defc318-29d6-4f37-3f93-95512ad1d600/public) - #### Check Products - Click **Check Products** to review product records. - At the top, you can: - **Create Products** - **Update Products** - **Delete Products** - Choose a brand - The list shows: - Code - Name - Bar Codes - Unit Price - Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e7ea0c66-81ec-4383-d627-a1f1ce385300/public) - #### Check Price - Click **Check Price** to review and update product prices. - The tabs include: - Update product price - Matched product price - Not created product - Unmatched product - Error product - Select a brand and click **Check** to load price data. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ede1292b-4cd4-49fd-2ae4-a2b99e47f700/public) - #### Check Customers - Click **Check Customers** to review customer records. - At the top, you can: - **Create customers** - **Update customers** - **Delete customers** - Choose a brand - The list shows: - MSD Code - Status - MSD Name - MSD Phone - MSD Email - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/94642325-5554-4c6c-77ff-401a30a41000/public) --- # [Erkhet Sync] Published 2026-10-09 > Mongolian Plugin — Manage Local Integrations and Syncs The Mongolian plugin contains tools for managing local Mongolian service integrations. It includes three main sections: Put Response , Erkhet Sync , and MSDynamic . **Mongolian Plugin — Manage Local Integrations and Syncs** The **Mongolian** plugin contains tools for managing local Mongolian service integrations. It includes three main sections: **Put Response**, **Erkhet Sync**, and **MSDynamic**. #### How to open the Mongolian plugin 1. In the left sidebar, click **Plugins**. 2. Select **Mongolian**. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4e261607-f897-4c1f-7907-6c8006575500/public) The Mongolian plugin menu shows: - **Put Response** - **Erkhet Sync** - **MSDynamic** - ## **Erkhet Sync**   - The **Erkhet Sync** section is used to synchronize data with the Erkhet accounting system. - #### Sync History - Click **Erkhet Sync** in the Mongolian menu. - Select **Sync History**. - The Sync History page shows: - Created At - User - Content Type - Content - Response - Error - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e4013c58-6fcd-4aa5-c36c-80ad280d7a00/public) - If no sync has been created, the page shows **No sync yet**. - #### Check Deals - Click **Check Deals** in the Erkhet Sync submenu to review deal records. - The page shows sync status counters such as: - checked - synced - skipped - pending - error - resynced - toSync - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a593ffc3-8b0e-41ec-2db4-e6139c44b400/public) - #### Check Orders - Click **Check Orders** to review POS order records. - The list shows: - Created At - Number - Total Amount - Paid At - Sync Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/582dbf81-b68b-4753-90ae-82ded5dd4d00/public) - #### Check Products - Click **Check Products** to review product records. - At the top, you can see action buttons: - **Create Products** - **Update Products** - **Delete Products** - The list shows: - Code - Name - Bar Codes - Unit Price - Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7daadf69-b65c-4c01-728e-beaaa93f6100/public) - #### Check Category - Click **Check Category** to review category records. - At the top, you can see action buttons: - **Create Categories** - **Update Categories** - **Delete Categories** - The list shows: - Code - Name - Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1d6d2e76-4c9c-4174-0e1a-4ced08d99e00/public) --- # [Put Response] Published 2026-10-09 > Mongolian Plugin — Manage Local Integrations and Syncs The Mongolian plugin contains tools for managing local Mongolian service integrations. It includes three main sections: Put Response , Erkhet Sync , and MSDynamic . **Mongolian Plugin — Manage Local Integrations and Syncs** The **Mongolian** plugin contains tools for managing local Mongolian service integrations. It includes three main sections: **Put Response**, **Erkhet Sync**, and **MSDynamic**. #### How to open the Mongolian plugin 1. In the left sidebar, click **Plugins**. 2. Select **Mongolian**. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6d6ebaa9-35ea-46b6-4813-cab35dbede00/public) The Mongolian plugin menu shows: - **Put Response** - **Erkhet Sync** - **MSDynamic** ## **Put Response** The **Put Response** section is used to manage bill and response data. #### Put Response main view 1. Click **Put Response** in the Mongolian menu. 2. The main list shows columns for: - Bill ID - Sub Bill IDs - Number - Date - Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5e9b875a-1719-4abb-4b74-32ebdf39d500/public) If no records exist, the page shows **No put response**. #### By Date view Click **By Date** in the left submenu to view records grouped by date. The columns include: - Date - Count - City Tax - VAT - Amount - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5278d82b-3dd2-40a0-674c-a33873151a00/public) #### Duplicated view Click **Duplicated** in the left submenu to view duplicate records. The columns include: - Date - Count - City Tax - VAT - Amount - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c73c09c2-e9ef-4ff0-85f1-73cc3207aa00/public) --- # [Нөөц үлдэгдэл] Published 2026-10-09 > Reserve Remainders The Reserve Remainders section (Нөөц Үлдэгдэл) shows reserve stock levels by product and branch. How to open Reserve Remainders In the left sidebar, click Accounting . Expand Inventory . Click Reserve ## **Reserve Remainders** The **Reserve Remainders** section (Нөөц Үлдэгдэл) shows reserve stock levels by product and branch. ### How to open Reserve Remainders 1. In the left sidebar, click **Accounting**. 2. Expand **Inventory**. 3. Click **Reserve Remainders** (Нөөц Үлдэгдэл). The list shows product, branch, department, unit, and reserve quantity. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/412676dc-d37c-4ed8-1064-e17e2400e100/public) ### Adding a reserve remainder 1. Click **\+ add-reserve-remainder**. 2. Select: - Branch - Department - Product category - Product - Reserve quantity - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/877c8480-ae1e-4cc7-81e2-3e7a11f66f00/public) 3. Click **Save**. The reserve quantity is updated in the list. --- # [Тооллого] Published 2026-10-09 > Safe Remainders The Safe Remainders section records physical stock counts. How to open Safe Remainders In the left sidebar, click Accounting . Expand Inventory . Click Safe Remainders (Тооллого). The list shows previous ## **Safe Remainders** The **Safe Remainders** section records physical stock counts. ### How to open Safe Remainders 1. In the left sidebar, click **Accounting**. 2. Expand **Inventory**. 3. Click **Safe Remainders** (Тооллого). The list shows previous stock counts with date, branch, department, and description. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8480c880-ca31-4921-3586-aad7a98d5a00/public) ### Creating a safe remainder 1. Click **\+ Add Safe Remainder**. 2. Fill in: - Date - Branch - Department - Product category - Description - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1ba35975-a2ff-4088-1375-8ff00212f800/public) 3. Click **Save**. The safe remainder detail page opens. It shows all products in the selected category. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2b25cfd7-20eb-4d78-a5ce-87a438d6ca00/public) ### Tabs in safe remainder detail The safe remainder detail has tabs for: - **Count** (Тооллого) - **Income** (Орлого) - **Expense** (Зарлага) - **Sales** (Борлуулалт) - **Adjustment** (Тохируулга) - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/630d447f-6728-413a-afed-b24f7d56f400/public) In the **Adjustment** tab, you can map accounts for income, expense, and sales transactions. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4d397b1b-ff4b-497e-dfe6-d3abd3962200/public) ### Submitting a safe remainder 1. In the **Count** tab, mark which products were counted. 2. Click **ReCalc** to recalculate quantities. 3. Click **Submit** to finalize the stock count. 4. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5f3d2da5-72cb-448c-0349-911498a49800/public) After submitting, the safe remainder appears in the list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a0f92343-c2ae-4b07-fac9-9358bb43fd00/public) --- # [Үлдэгдэл] Published 2026-10-09 > Accounting Inventory — Manage Live Stock, Stock Counts, and Reserve Levels The Inventory section inside the Accounting plugin helps you track current stock levels, perform physical stock counts, and manage reserve quanti **Accounting Inventory — Manage Live Stock, Stock Counts, and Reserve Levels** The **Inventory** section inside the **Accounting** plugin helps you track current stock levels, perform physical stock counts, and manage reserve quantities. ## **How to open Inventory** 1. In the left sidebar, click **Plugins**. 2. Select **Accounting**. 3. Expand **Inventory** (Барааны т.х). 4. Choose the section you need. The inventory menu includes: - Live Remainders (Үлдэгдэл) - Safe Remainders (Тооллого) - Reserve Remainders (Нөөц Үлдэгдэл) ## **Live Remainders** The **Live Remainders** page shows current stock levels for all products. The list shows: - Product code - Product name - Short name - Category - Unit price - Quantity - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b5190cc8-fd4e-4f19-d9c3-9a8980e20f00/public) ### Recalculating remainders To update live inventory calculations: 1. Click the **ReCalc Remainder** button at the top right. 2. Select filters such as: - Branch - Department - Product category 3. Click **Run**. 4. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b979012d-9b16-45a9-51e6-0f3c723d6b00/public) --- # [Мэдээ тайлан] Published 2026-10-09 > Check Sync The Check Sync section (Мэдээ Таталт) lets you review and sync financial data from other modules. How to open Check Sync In the left sidebar, click Accounting . Click Check Sync (Мэдээ Таталт). The Check Sync ## **Check Sync** The **Check Sync** section (Мэдээ Таталт) lets you review and sync financial data from other modules. ### How to open Check Sync 1. In the left sidebar, click **Accounting**. 2. Click **Check Sync** (Мэдээ Таталт). The Check Sync page has two tabs: - **Deal rule** (Хэлцлийн дүрэм) - **POS order rule** (POS захиалгын дүрэм) - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8be8c055-09a4-4b57-3ff9-e34b3a10d700/public) You can see sync status for each record: - checked - synced - skipped - pending - Error - resynced - toSync - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0e00f184-509a-42f8-513a-d7edbed4f300/public) To configure sync rules, click **Go to settings**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/97d8fe11-089c-4d6e-9738-db2a21527c00/public) --- # [Тайлан] Published 2026-10-09 > Reports The Reports section lets you generate financial reports. How to open Reports In the left sidebar, click Accounting . Click Reports (Тайлан). The reports menu includes: Account Statement (Дансны хуулга) Trial Bala ## **Reports** The **Reports** section lets you generate financial reports. ### How to open Reports 1. In the left sidebar, click **Accounting**. 2. Click **Reports** (Тайлан). The reports menu includes: - Account Statement (Дансны хуулга) - Trial Balance (Гүйлгээ баланс) - General Ledger (Ерөнхий дэвтэр) - General Journal (Ерөнхий журнал) - Cash Flow Statement - Money Supply Report - Receivables/Payables Reports - Inventory Reports - Fixed Asset Reports - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ca5b37ab-0b93-4f6e-7705-c6227963d400/public) ### Generating an account statement 1. In Reports, click **Account Statement** (Дансны хуулга). 2. Select the account category, account, account type, and balance options. 3. Choose customer, tags, company tags, branch, department, creator, and date range. 4. Click **View Report**. 5. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/20e40082-a1f3-404d-5700-61e10b4a4d00/public) The generated report shows: - Code - Name - Opening balance (Debit/Credit) - Transactions (Debit/Credit) - Closing balance (Debit/Credit) You can export the report to Excel or print it. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9f107939-4ab5-4f23-64db-35c601cbf800/public) ## **Accounting Settings** The **Accounting Settings** page contains all configuration options for the Accounting plugin. ### How to open Accounting Settings 1. Go to **Settings → Accounting**. The settings menu includes: - General settings - Accounts - Account categories - Account permissions - VAT rows - CIT rows - Deal rule - Deal return rule - POS order rule - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e4a81bee-fab2-4083-27cc-fcf9414aa100/public) ### Accounts Click **Accounts** (Данс) to view and manage your chart of accounts. The accounts list shows: - Code - Name - Category - Currency - Status You can import accounts or add new ones. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/74d157b4-33d2-4d0c-d9dd-4cae30a77f00/public) A sample chart of accounts is provided in the **AccountingDANS.csv** file, which includes account codes, names, categories, currencies, journal types, and statuses. ### Account Categories Click **Account Categories** (Дансны ангилал) to manage the hierarchical structure of accounts. The list shows: - Code - Name - Parent category - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/82688b7f-6f08-4904-39c7-f37f14aca800/public) A sample category structure is provided in the **AccountinCategory.csv** file, which includes category codes, names, parent IDs, descriptions, and statuses. ### Account Permissions Click **Account Permissions** (Дансны эрх) to control which users can access or edit specific accounts. The list shows: - Name - Code - Email - Level - Read/Write permissions - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e0f45b50-fa99-4a79-b530-a4f0cf7a1b00/public) ### VAT Rows Click **VAT Rows** (НӨАТ-ын мөр) to configure value-added tax calculation rows. Each row has: - Number - Name - Type (title, formula, normal, free) - Status - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fe8b132f-0aef-41bb-7930-9909355db600/public) A detailed sample of VAT row configuration is provided in the **AccountingNUAT.csv** file. ### CIT Rows Click **CIT Rows** (ХХАТ-ын мөр) to configure corporate income tax calculation rows. This section works similarly to VAT rows. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c58a4b9c-81a4-4024-3e68-f0d9402ea400/public) ## **Sync Rules** Sync rules determine how data from Sales and POS flows into Accounting. ### Deal Rule Click **Deal rule** (Deal дүрэм) to configure how sales deals are synced to accounting. Set the: - Code - Name - Board - Pipeline - Stage - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a85272b1-462b-4ea1-5fa1-e215cca23200/public) ### Deal Return Rule Click **Deal return rule** (Deal буцаалтын дүрэм) to configure how deal returns are synced. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/051ff2d3-fd48-4363-ba39-6ccb4d9f1e00/public) ### POS Order Rule Click **POS order rule** (Pos order дүрэм) to configure how POS orders are synced to accounting. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/21d03dbf-8ccc-4348-1dca-c9031d1e1f00/public) ## **Fixed Assets** The **Fixed Assets** section (Үндсэн Хөрөнгө) lets you configure and track fixed assets. ### How to open Fixed Assets 1. Go to **Settings → Accounting → Fixed Assets**. The fixed assets menu includes: - Account config - Categories - Assets - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4a0c9f29-20a3-469c-1672-b4a83792d900/public) ### Fixed Asset Categories Click **Categories** (Бүлэг) to manage fixed asset categories. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/78678c4f-78ff-44a6-ed01-478ce857cc00/public) ### Fixed Assets List Click **Assets** (Үндсэн хөрөнгө) to view all fixed assets. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c7f8e646-5f0e-4c6b-21fa-44ca89b2fe00/public) --- # [Бүрэн бус баримтууд] Published 2026-10-09 > Incomplete Documents The Incomplete Documents section (Бүрэн Бус Баримтууд) lists transactions that are missing required information or are not yet balanced. To open it, click Incomplete Documents in the Accounting menu. ## **Incomplete Documents** The **Incomplete Documents** section (Бүрэн Бус Баримтууд) lists transactions that are missing required information or are not yet balanced. To open it, click **Incomplete Documents** in the Accounting menu. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cfd6c31b-e800-4fc7-f53f-19319c332700/public) --- # [Журнал бичилт] Published 2026-10-09 > Journal Entries The Journal Entries section lets you view all posted accounting entries grouped by journal category. How to open Journal Entries In the left sidebar, click Accounting . Click Journal Entries (Журнал Бичил ## **Journal Entries** The **Journal Entries** section lets you view all posted accounting entries grouped by journal category. ### How to open Journal Entries 1. In the left sidebar, click **Accounting**. 2. Click **Journal Entries** (Журнал Бичилт). The left panel shows all journal categories. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fe8982f3-4221-4fd2-cdaa-baf345a02c00/public) ### Adding a journal entry 1. Click **\+ Add Transaction** at the top right. 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a1ea9c83-ec92-4229-b33a-89de3a7e1900/public) 3. Fill in the journal entry form. 4. Click **Save**. The entry is posted to the selected journal. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6ab45e44-263f-4746-6f7a-0dd929d35e00/public) ### Filtering by journal Click any category in the left panel to filter entries by that journal. For example, selecting **General** shows only general journal entries. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3104b75e-6650-448a-f040-ffde433bb200/public) Scroll down the category list to see fixed-asset-related journals and other categories. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b3f2e208-7680-419e-7428-214327611800/public) --- # [Баримтууд] Published 2026-10-09 > Accounting — Manage Transactions, Journals, Reports, and Tax Settings The Accounting plugin helps you manage your company's financial records, including transactions, journal entries, reports, tax calculations, accounts, **Accounting — Manage Transactions, Journals, Reports, and Tax Settings** The **Accounting** plugin helps you manage your company's financial records, including transactions, journal entries, reports, tax calculations, accounts, and fixed assets. ## **Transactions** The **Transactions** page is the main place to view and manage all accounting transactions. ### How to open Transactions 1. In the left sidebar, click **Plugins**. 2. Select **Accounting**. 3. Click **Transactions** (Баримтууд). The Transactions list shows: - Account (ДАНС) - Number (ДУГААР) - Date (ОГНОО) - Description (ТАЙЛБАР) - Status (ТӨЛӨВ) - Journal (ЖУРНАЛ) - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5e498ac4-6afc-4357-539e-406ddf1b0800/public) ### Creating a new transaction 1. Click the **\+ Add Transaction** button at the top right. 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d554a921-31b9-4212-0b07-e3844b07e100/public) 3. The transaction form opens. Fill in the main transaction details: - **Date** - **Number** - **Status** - **Creator** - **Confirmed by** - **Description** Then add journal lines with: - Account - Side (Debit/Credit) - Amount - Counterparty type and counterparty - Branch - Currency - Description - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/dca30711-42ec-4464-9d6d-3924c61c4500/public) You can add multiple product or material lines to a transaction. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0c82e6b2-e7a5-4050-06a4-4ecd3798d200/public) 1. Click **Save**. The new transaction appears in the Transactions list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3e1ecda1-048d-4c30-ec82-7d87187ed200/public) ### Using journals Every transaction belongs to a journal. You can select a journal category when creating or viewing transactions. Click the journal dropdown to choose from categories like: - General (Ерөнхий) - Cash (Касс) - Bank (Харилцах) - Receivables (Авлага) - Payables (Өглөг) - Inventory - Sales - Purchases - Fixed assets - --- # [Complete User Guide] Published 2026-10-09 > Segments — Complete User Guide Getting Started with Segments Path: Frontline → Core modules → Segments Segments let you group records by shared conditions. You can build segments for Team members, Companies, Customers, L ## **Segments — Complete User Guide** ## **Getting Started with Segments** **Path:** Frontline → Core modules → Segments Segments let you group records by shared conditions. You can build segments for Team members, Companies, Customers, Leads, Products, Tickets, Conversations, Tasks, Projects, Deals, and POS orders. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1f9c9cc3-6cec-4581-f885-829534291d00/public) ## **How to Create a Segment** Click **Create Segment** in the top-right corner. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/feaf1f67-2894-46b0-a8cc-66c874d37100/public) In the **Create a segment** modal, configure: - **Color** — pick a visual label. - **Name** — segment title. - **Description** — optional note. - **Who can see this** — visibility setting. - **Conditions** — choose the Type, Property, Condition, and Value. You can combine conditions with **AND** or **OR** logic, and add condition groups for nested rules. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ea615dc5-8e4c-4044-22ca-b1692403e500/public) The **Matching Records** counter updates as you build conditions. Click **Save segment** when done. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9894e680-afcf-4a69-0f55-04faf0239800/public) Saved segments appear in the list with their name, description, and matching count. ## **Segment Types and Examples** ### Company segments Filter companies by properties such as primary name. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a6c8f404-86ce-47c4-d253-fca08183a800/public) ### Customer segments Filter customers by status, lifecycle, or other fields. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1a90efc2-7ed1-4b08-b14e-0f6f1448bc00/public) ### Lead segments Filter leads by name or contact properties. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/87f962cd-514b-4a76-d2f1-6629075fbf00/public) ### Product segments Filter products by barcodes, categories, or other product fields. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/549153be-6abe-4bc3-f2be-7723b179c200/public) ### Ticket segments Filter support tickets by name, status, or assigned team. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6d1f5987-4dc1-4417-33e9-fbd2a9dcc600/public) ### Conversation segments Filter conversations by channel or message properties. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/332073f7-bd0f-4cbe-336f-c16529fccd00/public) ### Task segments Filter tasks by priority, due date, or assignee. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6c5bf7b6-bf3a-4767-8158-45f0f1f34900/public) ### Project segments Filter projects by linked segments or project properties. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c9533b99-2c02-4459-9d7b-fc0c05b32800/public) ### Deal segments Filter sales deals by name, stage, or value. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b82f6dda-6561-4c03-e7cd-b3e9b047c700/public) ### POS order segments Filter POS orders by order number or related fields. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/67198ffa-966a-4f67-343d-be20549b1800/public) --- # [Leads] Published 2026-10-09 > How to Create and Manage Leads Path: Frontline → Core modules → Contacts → Leads The Leads tab tracks potential customers before they are converted. Click Add Lead to create a new lead. Fill in the lead form: First Name ## **How to Create and Manage Leads** **Path:** Frontline → Core modules → Contacts → Leads The Leads tab tracks potential customers before they are converted. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/da71ea85-6bd4-4164-7986-ae15e0ace000/public) Click **Add Lead** to create a new lead. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/de293fc9-b36d-4483-f50d-edca25f6cc00/public) Fill in the lead form: - **First Name** and **Last Name** (required). - **Code** and **Owner**. - **Email** and **Email Verification Status**. - **Phone** and **Phone Verification Status**. - **Description**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9bf36c59-727a-4066-0c65-5872bf151900/public) Click **Create Lead**. The lead details page opens with a success message. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bf3b7b45-b2b4-47cb-1252-68999108d000/public) --- # [Client Portal Users] Published 2026-10-09 > How to Create and Manage Client Portal Users Path: `Frontline → Core modules → Contacts → Client Portal Users** Client Portal Users are contacts who can log in to a dedicated client portal. Click Add client portal user t ## **How to Create and Manage Client Portal Users** **Path:** \`Frontline → Core modules → Contacts → Client Portal Users\*\* Client Portal Users are contacts who can log in to a dedicated client portal. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/705a4fc3-9f80-4540-351d-cd8a6c5b8600/public) Click **Add client portal user** to create portal access. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bc9984ea-9e2e-4d60-cf46-c907b3036400/public) Fill in the portal user form: - **Client Portal** — select the portal they belong to. - **Type** — user type, such as Customer. - **Email** and **Phone**. - **First Name** and **Last Name**. - **Username** and **Password**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fd2f99cc-c1c4-4d3b-6b57-822197e25800/public) Click to create the user. The Client Portal User details page opens, showing verification status and linked customer. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/762d5168-cd9e-4a45-3dd0-ae5f610d0200/public) --- # [Companies] Published 2026-10-09 > How to Create and Manage Companies Path: Frontline → Core modules → Contacts → Companies The Companies tab stores business accounts. It starts empty if no companies have been added. Click Add Company to register a new co ## **How to Create and Manage Companies** **Path:** Frontline → Core modules → Contacts → Companies The Companies tab stores business accounts. It starts empty if no companies have been added. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0f0181f6-b260-48f3-dd9e-a758609f5700/public) Click **Add Company** to register a new company. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/83d92d04-2a98-4c0d-e0a1-333d0e23fc00/public) Fill in the company form: - **Name** and **Code**. - **Owner** and **Parent Company**. - **Email**, **Phone**, and **Website**. - **Industries**, **Headquarters Country**, **Business Type**, and **Size**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/55397919-9e0c-4f8e-320c-9868b7608400/public) Click **Create Company**. The company details page opens with a success message. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c4e00e4b-8742-4c58-265f-deb5170caa00/public) --- # [Customers] Published 2026-10-09 > Contacts — Complete User Guide Getting Started with Contacts Path: Frontline → Core modules → Contacts The Contacts module is the central place to manage people and organizations connected to your business. It has four t ## **Contacts — Complete User Guide** ## **Getting Started with Contacts** **Path:** Frontline → Core modules → Contacts The Contacts module is the central place to manage people and organizations connected to your business. It has four tabs: **Customers**, **Companies**, **Leads**, and **Client Portal Users**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6d26e101-f1cc-4227-7b1f-b1a78b9ef300/public) ## **How to Create and Manage Customers** **Path:** Frontline → Core modules → Contacts → Customers The Customers tab shows all registered customers. You can search, filter, import, export, and add new customers from here. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a82b3076-8322-4f9c-76d8-a62248d89f00/public) To create a new customer, click the **Add Customer** button in the top-right corner. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/282f5da6-f6c1-4043-fb72-064fd87b4d00/public) Fill in the customer form: - **First Name** and **Last Name** (required). - **Code** — internal reference code. - **Owner** — assigned team member. - **Email** and **Email Verification Status**. - **Phone** and **Phone Verification Status**. - **Description** — optional notes. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/64b05a14-0d57-49d7-d905-876184b43900/public) Click **Create Customer**. The customer details page opens with a success message. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bbf59b2f-3454-4da5-7379-fa8bcced2500/public) --- # [Coupons] Published 2026-10-09 > How to Create and Issue Coupons Step 1 — Open Coupon settings Path: Settings → Loyalty config → Coupon Click Add coupon campaign . Step 2 — Configure the coupon campaign On the Campaign tab, enter: Title , Buy Score , Co ## **How to Create and Issue Coupons** ### Step 1 — Open Coupon settings **Path:** Settings → Loyalty config → Coupon Click **Add coupon campaign**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d7b8dece-270a-47b4-83d6-9d8af545c400/public) ### Step 2 — Configure the coupon campaign On the **Campaign** tab, enter: - **Title**, **Buy Score**, **Count**, and **Kind**. - **Start Date / End Date**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ecc73aca-dc0f-408e-1006-c5f100b4a800/public) On the **Restriction** tab, set: - **Minimum Spend / Maximum Spend**. - Allowed or excluded **Product Category**, **Product**, and **Tag**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f5e9b7eb-31a5-4ea3-9a08-232cb1bbcc00/public) On the **Code rule** tab, choose how codes are generated. **Default** mode: - Set **Code Length**, **Number of Codes**, prefix/postfix uppercase count, **Character Set**, **Pattern**, **Usage Limit**, and **Redemption Limit Per User**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c9bb293e-d1cc-4463-e5c8-9f9f0fa37400/public) **Static** mode: - Enter a fixed **Code**, **Usage Limit**, and **Redemption Limit Per User**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ee1f2e4d-5d5e-4113-8bb1-6e26dae23a00/public) Save the campaign. The coupon appears in the campaign list. ### Step 3 — Issue a coupon **Path:** Frontline → Loyalty → Coupons The list shows created coupons with campaign, code, usage, limit, status, and creation date. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/213642f4-b746-4f0b-421e-b2fcae9f5c00/public) Click **Add coupon** to issue a coupon to a customer. - Select the **Campaign**. - Save. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/34179929-c113-41fc-6bc7-f1890254c400/public) --- # [Agents] Published 2026-10-09 > How to Create Agents Path: Frontline → Loyalty → Agents The Agents section lets you set up customer-specific loyalty rules, such as prepaid and discount percentages, with optional date and product restrictions. Click to ## **How to Create Agents** **Path:** Frontline → Loyalty → Agents The Agents section lets you set up customer-specific loyalty rules, such as prepaid and discount percentages, with optional date and product restrictions. Click to create a **New agent** and configure the form: - **Relevant Customers** — select the customers this agent applies to. - **Relevant Companies** — optionally limit to specific companies. - **Has Return** — toggle if the agent includes return logic. - **Prepaid Percent** — upfront payment percentage. - **Discount Percent** — discount percentage applied. - **Start Date / End Date** — full active date range. - **Start Month / End Month** — active month range. - **Start Day / End Day** — active day range. - **Choose Product Rules** — select which product rules apply. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ba0e8314-898c-41ef-4bd8-67b119bc4900/public) --- # [Assignments] Published 2026-10-09 > How to Create and Issue Assignments Step 1 — Open Assignment settings Path: Settings → Loyalty config → Assignment Click Add assignment campaign . Step 2 — Configure the assignment campaign Set: Title . Segment — target ## **How to Create and Issue Assignments** ### Step 1 — Open Assignment settings **Path:** Settings → Loyalty config → Assignment Click **Add assignment campaign**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4798168a-0209-4ad9-f560-7d39daf6bf00/public) ### Step 2 — Configure the assignment campaign Set: - **Title**. - **Segment** — target customer group. - **Voucher Campaign** — reward to assign. - **Start Date / End Date**. ### Step 3 — Issue an assignment **Path:** Frontline → Loyalty → Assignments Click **Add assignment**. - Select the **Campaign**. - Choose the **Owner**. - Save. - --- # [Scores] Published 2026-10-09 > How to Create and Give Scores Step 1 — Open Score settings Path: Settings → Loyalty config → Score Edit or create a score campaign. Step 2 — Configure the score campaign Set: Title , Description , and Order . Eligible or ## **How to Create and Give Scores** ### Step 1 — Open Score settings **Path:** Settings → Loyalty config → Score Edit or create a score campaign. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/275154e7-3e00-4156-34e7-6e74140c2700/public) ### Step 2 — Configure the score campaign Set: - **Title**, **Description**, and **Order**. - Eligible or excluded **Product Category**, **Product**, and **Tag**. - **Target** channel, such as Sales pipeline or POS Order. - Action type: **Add**, **Subtract**, or **Set**. - **Score Value**, **Currency Ratio**, and who to apply scores to. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f1d1e644-c5d8-4c7f-21df-8f6d4c516c00/public) ### Step 3 — Give a score manually **Path:** Frontline → Loyalty → Scores Open the score action to give points. - Select **Owner Type** and **Owner**. - Choose the **Target** channel. - Select the **Score Campaign**. - Enter the **Score** value and a **Description**. - Save. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d5a6f01c-e61b-47aa-0fc1-6f242ffbc100/public) --- # [Donates] Published 2026-10-09 > How to Create and Record Donations Step 1 — Open Donate settings Path: Settings → Loyalty config → Donate Click Add donation campaign . Step 2 — Configure the donation campaign Set: Title and Max Score . Linked Voucher C ## **How to Create and Record Donations** ### Step 1 — Open Donate settings **Path:** Settings → Loyalty config → Donate Click **Add donation campaign**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5b5926d4-8590-478b-9539-ac7cfdcb5f00/public) ### Step 2 — Configure the donation campaign Set: - **Title** and **Max Score**. - Linked **Voucher Campaign** and **Min Score**. - **Start Date / End Date**. ### Step 3 — Record a donation **Path:** Frontline → Loyalty → Donates Click **Add Donation**. - Select the **Campaign**. - Choose **Owner Type** and **Owner**. - Enter the **Donate Score**. - Save. --- # [Spins] Published 2026-10-09 > How to Create and Issue Spins Step 1 — Open Spin settings Path: Settings → Loyalty config → Spin Click Add spin campaign . Step 2 — Configure the spin campaign Set: Title and Buy Score . Spin levels with Name , Voucher C ## **How to Create and Issue Spins** ### Step 1 — Open Spin settings **Path:** Settings → Loyalty config → Spin Click **Add spin campaign**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cd6562ad-8790-4bb8-70ab-b1da14774e00/public) ### Step 2 — Configure the spin campaign Set: - **Title** and **Buy Score**. - Spin levels with **Name**, **Voucher Campaign**, and **Probability**. - **Start Date / End Date**. ### Step 3 — Issue a spin **Path:** Frontline → Loyalty → Spins Click **Add Spin**. - Select the **Campaign**. - Choose **Owner Type** and **Owner**. - Set **Status** and linked **Voucher Campaign**. - Save. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/db72f67b-8823-4a0f-75f6-ea9c904d2d00/public) --- # [Lotteries] Published 2026-10-09 > How to Create and Issue Lotteries Step 1 — Open Lottery settings Path: Settings → Loyalty config → Lottery If no lotteries exist, click Add lottery campaign . Step 2 — Configure the lottery campaign Fill in the form: Tit ## **How to Create and Issue Lotteries** ### Step 1 — Open Lottery settings **Path:** Settings → Loyalty config → Lottery If no lotteries exist, click **Add lottery campaign**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/25ab34ef-c7ef-4e2f-694a-adf3c739e600/public) ### Step 2 — Configure the lottery campaign Fill in the form: - **Title** and **Buy Score** — entry cost. - **Number Format** and length for lottery numbers. - Prize levels with **Name**, linked **Voucher Campaign**, and **Count**. - **Start Date / End Date**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/20dbb933-0c06-4dc7-c835-c63664d35200/public) ### Step 3 — Issue a lottery entry **Path:** Frontline → Loyalty → Lotteries Click **Add Lottery**. - Select the **Campaign**. - Choose **Owner Type** and **Owner**. - Set **Status** and linked **Voucher Campaign**. - Save. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5b758a85-6e2e-41e8-2f36-f6bf20044d00/public) --- # [Vouchers] Published 2026-10-09 > Loyalty — Complete User Guide Getting Started with Loyalty Path: Frontline → Loyalty The Loyalty module is where you build and manage customer reward programs. It includes Vouchers, Lotteries, Spins, Donates, Scores, Ass ## **Loyalty — Complete User Guide** ## **Getting Started with Loyalty** **Path:** Frontline → Loyalty The Loyalty module is where you build and manage customer reward programs. It includes Vouchers, Lotteries, Spins, Donates, Scores, Assignments, Agents, and Coupons. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9fb707c1-7456-4cc2-927e-0cd45a5bc400/public) Most loyalty items follow the same workflow: 1. Open the loyalty subsection. 2. Click **Go to settings** to configure the campaign rules. 3. Create the campaign with restrictions, dates, and rewards. 4. Return to the subsection and click **Add** to issue the reward to a customer. ## **How to Create and Issue Vouchers** ### Step 1 — Open Voucher settings From the Vouchers page, click **Go to settings** in the top-right corner. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0028f8ec-8e75-41f3-dc7e-236c378f6f00/public) You land on **Settings → Loyalty config → Voucher**. If no campaigns exist, click **Add voucher campaign**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ee787dea-878f-4520-2514-b451e81ede00/public) ### Step 2 — Configure the voucher campaign On the **Campaign** tab, enter: - **Title** — campaign name. - **Buy Score** — score cost to redeem. - **Type** — reward type, such as Product Bonus. - **Kind** — value type, such as Amount. - **Count** — number of vouchers available. - **Start Date / End Date** — active period. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6abac3a1-0493-47f8-1d97-0d74e0f3dc00/public) On the **Restriction** tab, set eligibility rules: - **Minimum Spend / Maximum Spend**. - Allowed or excluded **Product Category**, **Product**, and **Tag**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/02eb7b6b-c8ad-4021-c5e3-83a873b66d00/public) On the **Product Bonus** tab, choose the bonus item: - **Bonus Product** — product given when redeemed. - **Bonus Count** — quantity. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/607a7d66-39d5-4b0c-3ef7-355da6db4900/public) Save the campaign. It appears in the voucher campaign list, where you can toggle its status. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ba0bc80e-85e6-4924-65c8-dfe08d580800/public) ### Step 3 — Generate or issue a voucher To create a voucher code for a specific customer, click **Generate voucher**. - Select the **Voucher Campaign**. - Choose **Owner Type** and enter the **Owner ID**. - Set **Status** to Active. - Click **Generate**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ac11869c-493b-44ef-5d73-d9705b9b0c00/public) To issue a voucher from the main Vouchers list, click **Add Voucher**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fe06e71d-66f5-4349-e99f-89157682e200/public) Select the campaign, owner type, and customer, then save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6aaac796-2e0d-40d5-d31f-8e580bfabe00/public) The voucher now appears in the Vouchers list. --- # [Web Builder] Published 2026-10-09 > Web Builder — Complete User Guide Main Page / Overview Path: Frontline → Content → Web Builder What it does The Web Builder is a visual, drag-and-drop tool for creating and editing websites. You can create new web projec ## **Web Builder — Complete User Guide** ## **Main Page / Overview** **Path:** Frontline → Content → Web Builder ### What it does The Web Builder is a visual, drag-and-drop tool for creating and editing websites. You can create new web projects, choose a template type, build pages from pre-designed sections, customize styles, manage site pages, and publish the site. ### Main elements - **Left sidebar:** Content module with CMS and Web Builder options. - **Project cards:** Each card shows a website preview, name, description, template type, and action buttons. - **New Web Project button:** Starts a new website project. - **Build button:** Opens a project in the visual editor. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/931f621b-c626-4b5b-3f6f-b69214eaef00/public) ## **Creating a New Web Project** Click **\+ New Web Project** to start a new website. The New Web Project panel asks for: - **Name** — The project name, such as test-website. - **Client Portal** — The portal this website belongs to. - **Description** — A short explanation of the project. - **Domain** — The website domain or subdomain. - **Template Type** — Choose a template category such as E-Commerce, Hotel, or Tour. Click **Create** to generate the project. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3fcf7791-406c-420c-ce10-fba4d4069200/public) ## **Opening the Builder** From the project list, click the **build** button on a project card to open the visual editor. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3c5b016d-e634-41b7-b12b-356f4c4f3000/public) ## **The Builder Interface** The builder shows a live preview of the current page. The top bar displays the project and page name, device view buttons, and **Publish** / **Draft** controls. The left sidebar has three main tabs: **Layout**, **Style**, and **Settings**, plus **Pages** for managing site pages. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bc4200c5-a9fe-4e1f-67d8-6c012dd6d900/public) ## **Style Tab** The **Style** tab controls the visual identity of the site. It includes: - **Logo & Identity** — Upload the site logo. - **Colors & Appearance** — Set the site color scheme. - **Contact Information** — Add address, phone, social links, and email. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0c1f1b96-6367-4975-9bae-0101bfd05f00/public) ## **Pages Tab** The **Pages** tab lists every page on the site, such as Home, Profile, Booking, Privacy, Login, Register, Contact, Products, and About. Each page can be edited or deleted. Click **\+ Add Custom Page** to create a new page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8a298b9a-0940-46b7-922c-437991fdf400/public) ## **Settings Tab** The **Settings** tab contains advanced site options: - **Templates** — Change or manage the site template. - **Domains** — Connect custom domains. - **POS** — Configure point-of-sale integration. - **Delivery Info** — Set delivery-related settings. - **Delete Project** — Permanently remove the website. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d5aa584a-d915-4aac-c4d9-82e2903bf100/public) ## **Adding Sections** Click **\+ Add Main Section** to open the section library. Sections are grouped into categories such as Layouts and Lists. Available sections include Hero, Gallery, Carousel, Banner, Video, Image with Text, Text, Form, Messenger widget, Contact, Testimonials, Features, Stats, and more. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/241ea3e7-b1bd-48ee-8d78-d79cbdb0bd00/public) ## **Hero Section** The Hero section is a large top-of-page banner. Its settings include: - **Eyebrow** — A small label above the heading. - **Hero Title** — The main headline. - **Hero Description** — The supporting text. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b96441df-9533-48b0-d588-7bc5dfdf4c00/public) Scroll down to configure additional hero options: - **Commitments** — Short figure-and-label pairs shown under the hero. - **Hero Background** — Upload a background image. - **Primary and Secondary CTA Buttons** — Button labels and URLs. - **Background and Text Colors** — Choose default or custom colors. Click **Add section** to place it on the page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0522df1c-eee6-42e6-ac9c-6afaff8cff00/public) ## **Gallery Section** The Gallery section displays multiple images with text. Set the **Gallery Title**, **Gallery Description**, and upload images. You can also adjust the background and text colors. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b46f2d8c-e000-4c51-ea93-b6387d7b5e00/public) ## **Carousel Section** The Carousel section creates a sliding image gallery. Configure: - **Featured carousel** toggle. - **Description** — Text above the carousel. - **Carousel items** — Click **Add item** to create slides, each with a title, description, link type, and external URL. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/948249f0-3b02-4f37-0d28-c3fda7d81d00/public) ## **Banner Section** The Banner section highlights a single image with a link. Fields include: - **Title** (optional) - **Description** (optional) - **Banner image** — Upload the banner image. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0b63802f-4571-442a-6e7f-ad44b856e700/public) ## **Video Section** The Video section embeds a YouTube video. Set the **Video Section Title**, **Video Section Description**, and paste the **YouTube URL**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7acec7d9-40d5-4aab-ac95-0429a4994a00/public) ## **Image with Text Section** This section combines an image with formatted text. The top part of the settings includes: - **Eyebrow** — A small label. - **Title** — The section heading. - **Content** — Rich text with formatting tools. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/363b45ae-5be1-486c-e5a3-12d1be838700/public) The lower part includes: - **Image** — Upload an image. - **Image Position** — Choose left or right alignment. - **CTA Button** and **CTA URL** — Add a call-to-action. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2b9abfd9-4cca-43f4-9df8-a6b799b0f800/public) ## **Text Section** The Text section is a simple content block with a title and rich text body. It also supports a CTA button and URL. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ee3614a3-0a50-4bd7-4ff9-a4d1c2b27900/public) ## **Form Section** The Form section embeds a form from the erxes system. Configure: - **Form Section Description** - **Success Message** — Shown after submission. - **Choose your form** — Select an existing form and click **Activate** to use it. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/85ec691f-7e56-44ac-2176-cb2dedf3d400/public) ## **Contact Section** The Contact section displays contact information and a contact form. Fields include: - **Eyebrow** - **Contact Section Title** - **Contact Section Description** - **Choose your contact form** — Select a form from the list. A note reminds you that contact details can be edited under **Style → Contact Information**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7e6cea2b-0c8a-4bd2-febe-751e2aa54c00/public) ## **Testimonials Section** The Testimonials section shows customer quotes. Set the eyebrow, title, and description, then click **Add quote** to create testimonial entries. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0fdeeb6b-06a5-445c-a7cb-2ab16bc4e200/public) ## **Features Section** The Features section displays selling points as icon cards. Configure: - **Title**, **Description**, and **Pill icon**. - **Alignment** — Such as centered. - **Features** — Click **Add feature** to create feature cards. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7dde4180-e982-44ce-f6b5-d6f869549a00/public) ## **Stats Section** The Stats section shows figure-and-label pairs. Set the eyebrow, title, and description, then click **Add figure** to add statistics. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/28d33953-2899-4d74-1313-4c3c8af02b00/public) ## **Banner Section (Alternative Layout)** The Banner section can also be used as a standalone highlighted image with optional title, description, and banner image upload. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b2ba5fe6-cd5d-4dc0-61dc-51ee00b44800/public) ## **How It Works Section** The How It Works section displays numbered steps. Set the eyebrow, title, and description, then click **Add step** to build the step-by-step flow. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/59fbf66a-392a-4cc6-a599-ca9a35396f00/public) ## **Products Section** The Products section displays products from the catalog. Configure: - **Title** — Such as "Latest products". - **Description** - **Category** and **Tag** filters. - **Show last X products** — Set the number of products to display. A preview shows matching products. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/82379497-3b09-4b51-c1c9-7a3dbc710500/public) ## **Browse Categories Section** The Browse Categories section lets visitors explore product categories. Select which categories to display, such as products, desserts, main dish, salad, and soup. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e221210e-97c7-4b68-e497-0cbaae205900/public) ## **Product Preview Settings** Some product sections allow advanced options: - **Max products to show** — Limit the number of items. - **Preview customer ID** — Enter a customer ID to preview last-viewed products for that customer. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a7c11c6d-cff8-4045-0535-f438f37e9000/public) ## **Latest Blogs Section** The Latest Blogs section pulls posts from the CMS. Set the **Section title**, choose a **Category**, and set the **Post count**. A preview shows matching blog posts. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4a358f3d-5c6c-4442-49d9-77ecc5986f00/public) --- # [Settings] Published 2026-10-09 > Settings General settings The Settings page controls site-wide CMS options. Under General , you can set: Website Name — The name of the site. Short Description — A brief description. Client Portal Kind — The type of port ## **Settings** ### General settings The Settings page controls site-wide CMS options. Under **General**, you can set: - **Website Name** — The name of the site. - **Short Description** — A brief description. - **Client Portal Kind** — The type of portal, such as "nino portal". - **Domain** — The primary domain. - **Public URL** — The public-facing URL. Click **Save Changes** when finished. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e390403d-bd1e-4dfc-e8e4-6edbcaa84900/public) ### SEO settings Under **SEO**, configure default search-engine metadata: - **Default Meta Title** — Title template for pages. - **Default Meta Description** — Description template, up to 160 characters. - **Default OG Image** — The image shown when sharing on social media. - **Meta Keywords** — Comma-separated keywords. - **Robots Indexing** — Choose Index Site or No Index. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5aaea12a-76a3-4591-9722-4d9ebf9b2600/public) ### Analytics and integrations Scroll down to add tracking and script integrations: - **GA Tracking ID** — Google Analytics ID. - **Google Tag Manager ID** — GTM container ID. - **Custom Head Scripts** — Scripts injected into the <head> of every page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0115b0e1-61f7-4d2f-409e-68b23c6eaf00/public) ### Content URL settings Under **Content**, configure how post URLs are built: - **Post URL Path** — The prefix for post URLs, such as /posts. - **Post URL Field** — The field used in the slug, such as Post ID. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/71c583ac-e3a1-4064-2c7a-78820e71f800/public) ### Languages and appearance Set the languages and visual identity of the site: - **Supported Languages** — Add or remove languages. - **Default Language** — The primary language for content. - **Site Logo** — Upload the website logo. - **Favicon** — Upload the browser tab icon. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/af896873-e1c6-4819-f25e-2c8271897d00/public) ### Access control and deletion At the bottom of Settings: - **Access Control** — Choose Open Access or Assigned Members Only. - **Delete CMS** — Permanently delete the CMS and all its content. Use this with caution. --- # [Custom Post Types] Published 2026-10-09 > Custom Post Types Custom Post Types list Custom Post Types let you create new content types beyond the default Post and Page. The list shows each type's name, description, and creation date. Click + Add Custom Type to cr ## **Custom Post Types** ### Custom Post Types list Custom Post Types let you create new content types beyond the default Post and Page. The list shows each type's name, description, and creation date. Click **\+ Add Custom Type** to create one. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1fda3764-1db6-4cac-7057-c5987b0a5000/public) ### New Custom Type Fill in the type details: - **Label** — The singular name. - **Plural Label** — The plural name, used in menus and lists. - **Description** — A short explanation. - **Key** — A unique system key, such as test\_label\_name. Click **Create** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/57ed41b8-b55f-48d4-c6db-2c0c9d1e9900/public) The new custom type appears in the list and in the sidebar under Content Management. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/033a5844-b2fb-4e59-c495-d7d7c5570200/public) --- # [Custom Fields] Published 2026-10-09 > Custom Fields Custom Fields list Custom Fields let you add extra data fields to posts, pages, categories, and other content types. The list shows each field group by name and type. Click + Add Group to create a new group ## **Custom Fields** ### Custom Fields list Custom Fields let you add extra data fields to posts, pages, categories, and other content types. The list shows each field group by name and type. Click **\+ Add Group** to create a new group. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0e120344-e2e0-47ee-d967-c1d763e7e700/public) ### Add Field Group When creating a field group, you can: - **Label** — Name the group. - **Code** — A unique identifier for developers. - **Show On** — Select which content types use this group, such as Pages, Categories, or Posts. - **Show on specific pages / categories / posts** — Further limit where the group appears. Click **Create** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2871d539-a182-4c57-763e-a62bd1743200/public) The new group then appears in the Custom Fields list. --- # [Menus] Published 2026-10-09 > Menus Menus list Menus control the navigation links on your site. The list shows each menu label, URL, and kind (header, footer, or main). Use the All , Header , and Footer tabs to filter. Click + Add Menu to add a new i ## **Menus** ### Menus list Menus control the navigation links on your site. The list shows each menu label, URL, and kind (header, footer, or main). Use the **All**, **Header**, and **Footer** tabs to filter. Click **\+ Add Menu** to add a new item. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1ae50a9a-137e-4bd0-dfb0-66213bffd300/public) ### New Menu The New Menu panel lets you build navigation links: - **Language** — The language for the menu item. - **Label** — The text that appears in the menu. - **Link Type** — Choose the type of link, such as URL. - **URL** — The destination address. - **Open in New Tab** — Check this to open the link in a new browser tab. - **Kind** — Select whether this is a header, footer, or main menu item. - **Parent Menu** — Choose a parent for dropdown menus, or keep it as a top-level item. Click **Create Menu** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b2514777-fd30-4867-4863-05c39f35b600/public) After saving, the menu list updates and a success message confirms the change. --- # [Tags] Published 2026-10-09 > Tags Tags list Tags are short labels that describe the topic of a post or page. The Tags list shows the tag name, slug, and creation date. Click + Add Tag to add a new tag. New Tag In the New Tag panel, fill in: Language ## **Tags** ### Tags list Tags are short labels that describe the topic of a post or page. The Tags list shows the tag name, slug, and creation date. Click **\+ Add Tag** to add a new tag. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4d911584-d02e-4540-d647-310435a68800/public) ### New Tag In the New Tag panel, fill in: - **Language** — The language for the tag. - **Tag Name** — The display name. - **Slug** — The URL-friendly version. Click **Create Tag** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c7febf4b-af49-47c3-08a3-8649490e0e00/public) After creation, the new tag appears in the list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bc0d17e0-764c-4df4-fd6f-bfd64b0e4300/public) --- # [Categories] Published 2026-10-09 > Categories Categories list Categories help group related posts and pages. The list shows each category's name, description, parent category, and creation date. Click + Add Categories to create a new one. New Category The ## **Categories** ### Categories list Categories help group related posts and pages. The list shows each category's name, description, parent category, and creation date. Click **\+ Add Categories** to create a new one. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/122e4da1-bc70-4de8-0c5d-091716899800/public) ### New Category The New Category panel lets you define: - **Language** — The language for the category. - **Name** — The category name. - **Slug** — The URL-friendly identifier. - **Description** — A short explanation of the category. - **Parent Category** — Optionally place this category under another one. - **Status** — Active or inactive. - **Custom Fields** — Add extra fields if needed. Click **Create Category** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/89ec51c8-a4d6-4995-5826-bcccbef26b00/public) --- # [Pages] Published 2026-10-09 > Pages Pages list Pages are used for static content such as About Us, Contact, or Home. The Pages list shows the page name, parent page, URL slug, creation date, and last update. Click + Add Page to create a new page. Add ## **Pages** ### Pages list Pages are used for static content such as About Us, Contact, or Home. The Pages list shows the page name, parent page, URL slug, creation date, and last update. Click **\+ Add Page** to create a new page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/24673ae9-4f61-453a-f337-4c8747bdae00/public) ### Add / edit a page The page editor works similarly to the post editor. **Main fields:** - **Page Name** — The title of the page. - **Content** — The main editor for page text and headings. - **Language** — The language version. - **Parent Page** — Choose a parent if this page should be nested under another page. - **Slug** — The URL-friendly path, such as /about us. Use the **Publish**, **Draft**, and **Save** buttons at the top to control the page status. --- # [Posts] Published 2026-10-09 > CMS — Complete User Guide Main Page / Overview Path: Frontline → Content → CMS What it does The CMS (Content Management System) is where you create and manage all the content for your website or portal. It includes tools ## **CMS — Complete User Guide** ## **Main Page / Overview** **Path:** Frontline → Content → CMS ### What it does The CMS (Content Management System) is where you create and manage all the content for your website or portal. It includes tools for writing posts, building pages, organizing content with categories and tags, managing navigation menus, adding custom fields and post types, and configuring site-wide settings. ### Main elements - **Left sidebar:** Content module navigation with CMS selected. - **Content Management menu:** Posts, Pages, Categories, Tags, Menus, Custom Fields, Custom Post Types, and Settings. - **Top bar:** CMS name ("nino cms"), current section, language switcher (MN / EN), and action buttons such as Add Post. - **Main table:** Lists existing records with columns for Name, Status, Categories, Tags, Author, and more. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/19ceb693-7ea0-4d9c-14ae-0e099a78ff00/public) ## **Creating and Managing Posts** ### Posts list The Posts page shows every article or blog post in the CMS. Each row displays the post name, status (published, scheduled, or draft), categories, tags, author, and view count. Use the **Filter** button to narrow results, and click **\+ Add Post** to create a new post. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/45c0140b-f12d-4f75-0e9b-5392b0ac7000/public) ### Add / edit a post When you open a new post, you see a content editor and a right-side settings panel. **Main fields:** - **Post Title** — The headline of the post. - **Content** — The main text editor where you write the body of the post. - **Language** — The language version you are editing (MN or EN). - **Post Type** — Choose from available types such as Post. - **Post ID** — Generated automatically by the system after saving; used in public URLs. - **Short Description** — A brief summary, up to 500 characters. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/62a30f4a-7f8b-4c29-ba59-8ad7f079bd00/public) **Metadata and organization:** - **SEO** — Expand this section to add search-engine-friendly titles and descriptions. - **Category** — Assign one or more categories. - **Tag** — Assign one or more tags. - **Featured** — Toggle to mark the post as featured. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/df742181-60ee-4769-e1fe-ff93e66ecc00/public) ### Publishing options At the top of the editor you can choose how the post is published. - **Publish** — Make the post live immediately. - **Draft** — Save the post without publishing. - **Scheduled** — Set a future date and time for automatic publishing. - **Auto Archive** — Optionally set the post to archive automatically. - **Save Draft / Save** — Store your work. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0bed65ce-ca80-4f2a-7912-703a10c6a700/public) When **Scheduled** is selected, a date picker and time picker appear so you can choose the exact publishing moment. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4e9d4eb4-bf30-4d6d-70dd-4f90b9908400/public) ### Posts list after creation After saving or scheduling, the new post appears in the list with its status. In the example below, "Test123" is shown as scheduled, while "nino's post" is published. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6c0d476e-1d38-43af-cf68-13e7ac7ac600/public) --- # [Hermes Agent] Published 2026-10-09 > Company Brain — Complete User Guide Main Page / Overview Path: Frontline → Agents → Company Brain What it does Company Brain is where you create and manage AI assistants for your organization. It includes two main assist ## **Company Brain — Complete User Guide** ## **Main Page / Overview** **Path:** Frontline → Agents → Company Brain ### What it does Company Brain is where you create and manage AI assistants for your organization. It includes two main assistant types: OpenClaw Assistant, a multi-platform AI with memory and community skills, and Hermes Agent, a self-improving AI that learns workflows and creates skills over time. ### Main elements - **Left sidebar:** Agents module with Company Brain selected. - **Sub-menu:** OpenClaw Assistant, Hermes Agent, Social / Postiz, OpenDesign Studio, Supermemory Engine, and Paperclip Orchestrator. - **Main area:** Lists existing assistants or shows an empty state. - **Add button:** Creates a new assistant. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3d252052-6811-4538-d3e5-ae2190f37d00/public) ## **Hermes Agent** Hermes Agent is a self-improving AI that learns from workflows and auto-creates skills. The Hermes Agent page shows an empty state until you add your first assistant. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/28559ffd-da16-438f-0458-3c1e6c7f3e00/public) ## **Creating a Hermes Assistant** Click **\+ Add Hermes Assistant**. In the first step, enter: - **Setup Type** — Create new server. - **Identifier Name** — A unique name. - **Description** — The purpose of the assistant. Click **Continue**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/29471836-bd01-4b81-0927-a55cc5345b00/public) ## **Hermes Provider Selection** Select an AI provider, enter the API key, and choose a model. In the example, OpenRouter is selected with the Auto Router model. Click **Connect erxes AI Assistant** to provision the runtime. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1c5e8696-a3bc-4385-3b7a-5a95280b1100/public) ## **Hermes Runtime Provisioning** The system builds the Hermes runtime, which usually takes 1–2 minutes. Steps include: - Preparing deployment - Creating an isolated workspace - Attaching persistent storage - Starting the Hermes runtime - Configuring the AI connection - Verifying runtime health A notification confirms when the runtime is provisioning. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/918fdfdd-c3e9-4c41-412f-52fc5b166100/public) ## **Hermes Chat Interface** Once ready, the Hermes chat interface opens. It shows: - **Top toolbar:** Refresh, search, undo, gamepad, share, and delete options. - **Left sidebar:** Tabs for chat, calendar, grid, memory, files, profile, tasks, analytics, and documents. - **Main area:** Welcome message with prompts like: - What files are in this workspace? - What's on my schedule today? - Help me plan a small project. - **Message input:** Type a message to Hermes. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b6fc0396-4768-47fa-52a6-4fa2d1c53a00/public) ## **Hermes Sidebar and Toolbar** The Hermes interface provides quick-access tools in the left sidebar and top toolbar. Use these to switch between conversations, scheduled tasks, files, analytics, and other agent features. --- # [OpenClaw Assistant] Published 2026-10-09 > Company Brain — Complete User Guide Main Page / Overview Path: Frontline → Agents → Company Brain What it does Company Brain is where you create and manage AI assistants for your organization. It includes two main assist ## **Company Brain — Complete User Guide** ## **Main Page / Overview** **Path:** Frontline → Agents → Company Brain ### What it does Company Brain is where you create and manage AI assistants for your organization. It includes two main assistant types: OpenClaw Assistant, a multi-platform AI with memory and community skills, and Hermes Agent, a self-improving AI that learns workflows and creates skills over time. ### Main elements - **Left sidebar:** Agents module with Company Brain selected. - **Sub-menu:** OpenClaw Assistant, Hermes Agent, Social / Postiz, OpenDesign Studio, Supermemory Engine, and Paperclip Orchestrator. - **Main area:** Lists existing assistants or shows an empty state. - **Add button:** Creates a new assistant. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e7afb1ce-4222-4c3d-31bd-08e1929bfa00/public) ## **Creating an OpenClaw Assistant** Click **\+ Add OpenClaw Assistant** to start. In the first step, fill in: - **Setup Type** — Choose to create a new server or link an existing assistant server from another SaaS. - **Identifier Name** — A unique name for the assistant. - **Description** — What the assistant will be used for. Click **Continue** to proceed. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0b83f3aa-9923-424b-3132-464d91e22300/public) ## **Choosing an AI Provider** In the next step, select the AI provider and model. Available providers include Kimi For Coding, Moonshot Kimi, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, DeepSeek, xAI Grok, and GLM (Z.AI). After selecting a provider, enter the required API key and choose a model from the dropdown. Click **Connect erxes AI Assistant** to continue. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/083c071b-ee61-4c59-0ecf-ee143af9ea00/public) ## **Provisioning the Assistant** The system creates a private runtime for the assistant. This process usually takes 2–4 minutes and includes: - Preparing deployment - Creating a private runtime - Starting the assistant - Installing tools and workspace - Verifying runtime health When the runtime is ready, you can click **Open assistant** to start using it. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0d337d3b-07b2-466d-4a9b-151c9ec29c00/public) ## **Assistant Card** After provisioning, the assistant appears as a card showing: - **Status** — Ready. - **Edit assistant** — Change assistant settings. - **Connect Discord** — Link the assistant to a Discord server. - **Name and description** — The identifier and purpose. - **Provider** — The connected AI provider. - **Invited members** — Team members with access. Click **Open AI Assistant** to launch the chat interface. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9d0bb4c4-9ef6-48a3-86d6-be5792323a00/public) ## **Connecting Discord** Click **Connect Discord** to make the assistant available in a Discord server. The panel shows: - **Runtime status** — Ready or another status. - **Server** and **Server ID** — The managed assistant server. - **Discord server** — Select the server to install the assistant into. - **Discord channel** — Choose the channel where the assistant will respond. Click **Connect channel** to finish. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ae1dc199-b8cf-45a3-2031-36f29ca49600/public) ## **Using the Assistant in Discord** Once connected, the assistant appears in the selected Discord channel. Users can mention or message the assistant, and it will reply in the channel. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/535e9522-232a-4d18-333c-a471b3072a00/public) ## **Opening the AI Assistant** Back on the assistant card, click **Open AI Assistant** to use the built-in chat interface. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/67b7bb40-fd34-49f9-61dc-aab56086ec00/public) ## **AI Assistant Chat Interface** The chat interface is where you interact with the assistant. It shows: - **Top bar:** Refresh, members, files, tools, share, and delete options, plus the active model dropdown. - **Main chat area:** A welcome message and suggested prompts such as: - What can you do? - Summarize my recent sessions - Help me configure a channel - Check system health - **Message input:** Type a message or use the microphone button for voice input. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/61bc927b-0ce1-4876-403e-bd08060d8e00/public) ## **Assistant Response and Usage Stats** When you send a message, the assistant replies. A side panel shows: - **Context window** — Current usage compared to the limit. - **Latest run tokens** — Input, output, and estimated cost. - **Cost by type** — Breakdown of input, output, cache read, and cache write costs. - **Provider and model** — The active provider and model name. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b3c530ab-60f4-4cdc-868e-b3c4637b2a00/public) ## **Model Settings** Click the model selector at the bottom of the chat to adjust settings. The panel lets you: - Change the **provider**. - Adjust **reasoning** level on a scale from Faster to Smarter. - Set the **speed** — Default, Fast, Standard, or Auto. - Choose whether to use the default model. - Save or discard changes. --- # PMS — How to Create and Manage Reservations Published 2026-09-29 > PMS — How to Create and Manage Reservations :root { --bg: #f8fafc; --surface: #ffffff; --surface-2: #f1f5f9; --surface-3: #e2e8f0; --text: #0f172a; --text-secondary: #475569; --text-muted: #64748b; --accent: #4f46e5; --a PMS — How to Create and Manage Reservations [PMS Guide](#) [Sign In](#entry) [Reservation](#reservation) [Details](#detail) [Payment](#payment) [Settings](#config) 2026 updated edition ## PMS — Complete guide to creating and managing reservations A complete guide on how to create reservations and manage hotel rooms, guests, and payments in the PMS (Property Management System). [Get Started →](#entry) 1 ## Signing in to PMS Sign in to the erxes system, then open the Tourism plugin from the left sidebar. Inside Tourism, click PMS to enter the property management system. ### Steps 1. Sign in to the erxes system. 2. Select the **Tourism** plugin from the left sidebar. 3. Click **PMS** (Property Management System) to open it. 4. Create a reservation using the **Create reservation** button in the top-right corner. ![Navigating to Tourism - PMS](https://profile-media.erxes.io/cms/70df6df0f78ca4c94473d05c62820df3.png) Navigate to Tourism → PMS ![Frontdesk view](https://profile-media.erxes.io/cms/2a92ad4298897807e5f390df51d8fd22.png) Frontdesk — room reservation view **Good to know:** The Frontdesk view shows each room listed by day and reservation status. At the bottom you can see reservation statuses: **Inquiry, Reserved, Modified, Cancelled**. 2 ## Creating a Reservation Click the **Create reservation** button on the Frontdesk page to open the reservation form. ### Steps 1. Select the **Check-in / Check-out** date and time. 2. **Room type** — choose the room type (e.g., Room — available count and price are shown). 3. **Select room** — choose a specific room (e.g., Twin bed room). 4. **Guests by room** — set how many adults and children will stay. 5. **Price summary** — review the total price. 6. Fill in **Guest information** on the right: choose Individual or Company, then enter name, email, and phone. 7. Click the **Create reservation** button. ![Reservation creation form](https://profile-media.erxes.io/cms/d5d08b91a21355cecc55e3505ba646fa.png) Create new reservation — main reservation form ![Selecting room type](https://profile-media.erxes.io/cms/03f19c60ea0df13ede7c177d7530d7e7.png) Room type — available room types and prices ![Selecting room and guests](https://profile-media.erxes.io/cms/995b661cea71c585526030d77ed8cbed.png) Select room, Guests by room, and Price summary ![Guest information](https://profile-media.erxes.io/cms/3ec78619979c560494582a8ba3906279.png) Guest information — choose Individual / Company and fill in the details **Success:** When the reservation is created, a "Reservation created successfully" message appears, and the reservation is visible on the Frontdesk view for the selected dates (Erxes …). ![Created reservation](https://profile-media.erxes.io/cms/da4cac398179d6cd683dfdb6ccb6fb4f.png) Reservation created successfully — visible on the Frontdesk view 3 ## Reservation Details Click a created reservation to view detailed information and edit it. There are four tabs. ### Tab Descriptions Tab Description **General information** Reservation name, notes, check-in/check-out date and time **Guests information** Number of guests, linked guest (Search guest / Search company), New guest **Rooms information** Rooms, prices, Add room, Add extras **Payment** Financial summary and payment processing ![General information tab](https://profile-media.erxes.io/cms/8e27f0c7a035453f20edad9095154c75.png) General information — basic reservation details ![Guests information tab](https://profile-media.erxes.io/cms/61e6e2f6ab9bd62c005abacbbf93f9af.png) Guests information — connect, edit, or remove guests ![Rooms information tab](https://profile-media.erxes.io/cms/39c2b83503c75002290ebe522c45e605.png) Rooms information — rooms, prices, Add room, Add extras 4 ## Adding Extras Use the **Add extras** button in the Rooms information tab to add minibar and other extra products to the selected room. ### Steps 1. Open the **Rooms information** tab. 2. Click the **Add extras** button for the room you want to add products to. 3. Select a category, then search for and choose the product. 4. Click **Save changes**. ![Add extras window](https://profile-media.erxes.io/cms/99d9ae52f2711665d30e62b116ea07dd.png) Add extras — choose minibar and other extra products 5 ## Processing Payments The Payment tab shows the Financial summary and lets you receive payments. ### Steps 1. Open the **Payment** tab. 2. Review the Financial summary: Room charges, Products & services, Subtotal, Total amount, Remaining amount. 3. Use **Add discounts** or **Print temporary receipt** if needed. 4. Click the **Process Payment** button. 5. Enter the Payment amount, Payment method (e.g., Cash), and Payment note, then receive the payment. ![Payment tab](https://profile-media.erxes.io/cms/6da0dd3bb69c9f6dcc79e429918e2f16.png) Payment — Financial summary, Add discounts, Process Payment ![Process Payment window](https://profile-media.erxes.io/cms/249deaa225e3a71437cf26260155b563.png) Process Payment — enter amount, method (Cash), and note **Note:** In Process Payment, you can choose which part to receive payment for from the Rooms / Extras / Remaining tabs. After payment is processed, the Remaining amount is updated. 6 ## PMS Settings (Edit PMS) From the Action menu on Tourism → PMS, open Edit PMS and configure the core PMS settings in five steps. ### Five Configuration Steps Step Description **1\. General information** Name, description, check-in/check-out times and prices (e.g., 14:00 / 20000, 12:00 / 20000), Add discount, Website reservation lock **2\. Payments** Select payment methods, Erxes app token, Other payments (e.g., golomtCard, khaanCard, TDBCard) **3\. Permissions** Assign users to roles: General managers, Managers, Reservation managers, Reception, Housekeeper **4\. Appearance** Main logo, Primary / Secondary / Third colors, Website url **5\. Pipeline config** Board, Pipeline selection, Room categories, Extra product categories, Appointment product categories ![General information settings](https://profile-media.erxes.io/cms/35bcdfb653329bf99099ffb32b6a6bd3.png) 1\. General information — name, times, prices, discount ![Payments settings](https://profile-media.erxes.io/cms/08e805deb02d5451206b712b70f198c4.png) 2\. Payments — payment methods, token ![Permissions settings](https://profile-media.erxes.io/cms/6c97dbf6fc7720abf8e482b2652b6da5.png) 3\. Permissions — role assignments ![Appearance settings](https://profile-media.erxes.io/cms/0c26d7fe06332ff4fe87349b376432ef.png) 4\. Appearance — logo, colors ![Pipeline config settings](https://profile-media.erxes.io/cms/00dd0e2ff3cf4001d412b5241447575a.png) 5\. Pipeline config — Board, Pipeline, Room categories, Extra product categories, Appointments **Important:** In Pipeline config, under **Room categories**, only room categories should be selected (e.g., R-01 Room). Do not select the Price category (P-01) for rooms — pricing is handled separately. ![Selecting room categories](https://profile-media.erxes.io/cms/4a95039fd97cb28ca992a254a05f74c0.png) Selecting Room categories — R-01 Room (not P-01 Price) 7 ## Creating Categories (Settings → Products) Create the room, price, and room type categories needed for PMS under Settings → Products → Categories. ### Steps 1. Click **Settings** in the bottom-left corner. 2. Select the **Products** menu. 3. Open the **Categories** tab. 4. Create categories using **Add Category**: Price (P-01), Room (R-01), King room (K-01), Twin room (T-1), etc. ![Products - Categories](https://profile-media.erxes.io/cms/c579bad0a1289591cb468cc8e018b463.png) Settings → Products → Categories — Price, Room, King room, Twin room categories **Order:** First create categories here, then create the corresponding products/rooms, and finally link them in the PMS settings under Pipeline config. ### Have questions? This guide helps you create reservations, manage hotel rooms, and process payments in PMS. [Read again ↑](#entry) --- # [Channels] Published 2026-09-22 > Frontline Channels — Complete User Guide 1. Channels List Page Path: Settings → Frontline → Channels What it does The Channels page shows all the communication channels your team uses to talk with customers. Each channel ## **Frontline Channels — Complete User Guide** ## **1\. Channels List Page** **Path:** Settings → Frontline → Channels ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/348328fe-c343-4af5-9b6c-a330e384b000/public) ### What it does The Channels page shows all the communication channels your team uses to talk with customers. Each channel can have its own members, forms, surveys, response templates, pipelines, and integrations. ### What is on the page At the top: - **Filter** — Use this to search or filter channels. - **14 records found** — Shows how many channels exist. - **\+ Create channel** — Button to create a new channel. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ac25d9e6-5c47-4389-1576-7147c41ce200/public) The table columns: - **TITLE** — The name of the channel. - **DESCRIPTION** — A short explanation of the channel. - **MEMBERS** — How many team members are in the channel. - **PIPELINES** — Number of ticket pipelines. - **FORMS** — Number of forms connected to the channel. - **TEMPLATES** — Number of response templates. - **INTEGRATIONS** — Connected apps like Messenger, Facebook, Instagram, etc. - **STATUS** — Active or inactive. - **CREATE** — When the channel was created. ## **2\. How to Create a Channel** 1. Click **\+ Create channel** at the top right. 2. A **Create channel** panel opens on the right side. ### Fields to fill: - **ICON** — Choose an icon for the channel. - **NAME** — Enter the channel name. Example: test by temuulen - **DESCRIPTION** — Add a short description. Example: hi, test channel - **MEMBERS** — Click the dropdown and select team members to add. 1. Click **Create** at the bottom. 2. The new channel appears in the list. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3c3fc780-5b80-4bcd-44d8-ee99f954c900/public) ## **3\. Channel Detail Page** **Path:** Click on a channel name from the list. ### What it shows At the top you see: - The channel name, for example **test by temuulen**. - Editable fields: **ICON**, **NAME**, and **DESCRIPTION**. - An **Update** button to save changes. Below that, you can manage different parts of the channel: ### Manage channel members - Shows how many members the channel has. - Click to open the members page. ### Manage ticket pipelines - Shows how many pipelines exist. - Used to organize customer support tickets. ### Manage channel forms - Shows how many forms exist. - Forms are used to collect information from customers. ### Manage channel surveys - Shows how many surveys exist. - Surveys are used to ask questions and collect feedback. ### Manage channel response templates - Shows how many saved responses exist. - These are quick replies your team can use. ### Integrations At the bottom, you can connect communication tools such as: - **erxes Messenger** — Chat widget on your website. - **Facebook Messenger** — Connect Facebook messages. - **Facebook Post** — Manage comments on posts. - **Instagram Messenger** — Instagram Direct messages. - **Instagram Post** — Instagram comments. - **Discord** — Discord channel messages. - **Call** — Phone calls. - **Call Pro** — Advanced phone system. - **Email** — Receive emails as conversations. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e71d5f86-b401-449e-d5c3-0e0ae2222b00/public) ## **4\. Manage Channel Members** **Path:** Channel detail → Manage channel members ### What it does This page shows all team members who belong to the channel. ### What is on the page - A table with columns: **MEMBER** and **ROLE**. - Example: Temuulen(SM) Tserenchanaa with role Admin. - **\+ Add members** button to invite more people. To add members: 1. Click **\+ Add members**. 2. Select the users you want. 3. Assign a role if needed. 4. Save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5e54414c-a05c-445f-401e-670e7510e900/public) ## **5\. Manage Ticket Pipelines** **Path:** Channel detail → Manage ticket pipelines ### What it does Pipelines help you organize customer support tickets into stages, like "New", "In Progress", and "Resolved". ### What is on the page - If empty, you see: **No pipelines yet**. - Text: _"Get started by creating your first pipeline to organize and manage your workflow processes."_ - **\+ Create pipeline** button. To create a pipeline: 1. Click **\+ Create pipeline**. 2. Enter a **Pipeline name**. 3. Add a **Description** if needed. 4. Click **Create**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a6b625e0-bf12-4d07-6b48-b9b35d199600/public) ## **6\. Manage Channel Forms** **Path:** Channel detail → Manage channel forms ### What it does Forms let you collect information from customers. For example, a contact form or feedback form. ### What is on the page - If empty: **No forms found**. - Text: _"Create a form to start collecting data."_ - **\+ Create form** button. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b2ad77b4-f784-4651-2599-de8b1bcef400/public) ## **7\. How to Create a Form (Step-by-Step)** Click **\+ Create form**. The form builder has 4 steps. ### Step 1 — General - **TITLE** — Name of the form. Example: title - **DESCRIPTION** — Short description. - **PRIMARY COLOR** — Choose the main color. - **APPEARANCE** — Choose between **Iframe** or **Messenger**. - **LOAD TYPE** — Choose **Embedded** or **Popup**. - **BUTTON TEXT** — Text on the submit button. - **CHANNEL** — Select which channel this form belongs to. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3d47b167-3cfb-4cfc-5a94-ad343c51d300/public) ### Step 2 — Callout This is the introduction screen before the form. - **Skip callout** — Toggle to skip this screen. - **TITLE** — Callout title. - **BODY** — Callout message. - **BUTTON TEXT** — Button text to start the form. - **IMAGE** — Upload an image. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b977a2cd-cc28-4449-7d49-e5ff7ec64400/public) ### Step 3 — Content This is where you add the actual form fields. - Click **\+ Add Field** to add questions. - You can also click **\+ Add Step** to split the form into multiple pages. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/56e10284-bdd8-431c-3704-329475ca8f00/public) ### Step 4 — Confirmation This is the message shown after the form is submitted. - **TITLE** — Example: title - **DESCRIPTION** — Example: description - **IMAGE** — Upload an image. Click **Update form** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3e19d9e0-fda7-4adf-ef05-7d4fe6094c00/public) ## **8\. Manage Channel Surveys** **Path:** Channel detail → Manage channel surveys ### What it does Surveys let you ask customers questions inside messenger conversations. ### What is on the page - If empty: **No surveys found**. - Text: _"Create a survey and send it into a messenger conversation."_ - **\+ Create survey** button. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/37abe7d4-6091-4953-535a-c0bb2c994600/public) ## **9\. How to Create a Survey (Step-by-Step)** Click **\+ Create survey**. ### Step 1 — General - **TITLE** — Name of the survey. Example: test2 - **BRAND** — Select which brand this survey belongs to. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8680a365-7bdc-4411-7bcd-4047bf780400/public) ### Step 2 — Content Add the questions for your survey. - **STEP** — Name the step. Example: Step 1 - **QUESTION** — Example: How are you? - **OPTIONS** — Add answer choices. Example: good, bad - Click **\+ Add option** to add more choices. - **ALLOW MULTIPLE ANSWERS** — Toggle if users can select more than one answer. - Click **\+ Add Step** to add more questions. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4d90be10-e42d-4506-9e39-9c631edfc200/public) ### Step 3 — Confirmation - **DURATION** — Choose when the survey ends. Example: **No end date**. - Review the survey steps and questions. Click **Create survey**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8ec2f298-7919-4fb6-6a64-dc920f504700/public) After saving, the survey appears in the list with details: - Name, question, steps, status, created by, survey sent count, created date. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/950fba9d-b448-4356-dc29-f13d9a197b00/public) ## **10\. Manage Channel Response Templates** **Path:** Channel detail → Manage channel response templates ### What it does Response templates are saved messages your team can quickly send to customers. ### What is on the page - If empty: **No responses yet**. - Text: _"Get started by creating your first response."_ - **\+ Create response template** button. To create a template: 1. Click **\+ Create response template**. 2. Enter a **NAME**. 3. Enter the **CONTENT** of the message. 4. Click **Create**. 5. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4048c861-c612-4e47-f8de-96027d2b5000/public) ## **11\. Channel Integrations** **Path:** Channel detail → Integrations section ### What it does Integrations connect external messaging platforms to your channel. ### Available integrations - **erxes Messenger** — Website chat widget. - **Facebook Messenger** — Facebook private messages. - **Facebook Post** — Comments on Facebook posts. - **Instagram Messenger** — Instagram Direct messages. - **Instagram Post** — Comments on Instagram posts. - **Discord** — Discord server messages. - **Call** — Phone call handling. - **Call Pro** — Advanced PBX call system. - **Email** — Team inbox email address. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/18353189-b79b-4665-90ac-116732016a00/public) ## **12\. How to Add an erxes Messenger Integration** This is a 6-step setup. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6c4619e4-3961-4fcc-d9f2-ecaebf55d700/public) ### Step 1 — Appearance Customize how the messenger looks. - **Brand Color** — Main brand color. - **Text Color** — Text color. - **Background Color** — Background color. - **Logo** — Upload a logo for the messenger header. - **Launcher Logo** — Upload the floating chat button icon. - **Hero Style** — Choose background style: **Glossy**, **Aurora**, **Mesh**, or **Flat**. - **Navigation Bar** — Choose style: **Pill** or **Fluid**. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0d6515d9-10a9-4924-0f35-1726a6712600/public) ### Step 2 — Greeting Set up the greeting screen. - **Greeting Title** — Example: Test1 - **Greeting Message** — Example: greeting from temuulen - **Supporters** — Select support team members. - **Social Links** — Add links to social media. You can see a live preview on the right. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6cb98334-2703-4d3c-c1d3-d3acc15dfd00/public) ### Step 3 — Intro Set up automatic messages. **Online Messaging:** - **Welcome Message** — Example: welcome to my chat greeting from temuulen **Offline Messaging:** - **Away Message** — Example: sorry we are away from message - **Thank You Message** — Example: thank you for choosing our system Click **Next step**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b5c3a20a-8ac5-4482-90d0-da8d8d7ee100/public) ### Step 4 — Hours Availability Set when the messenger is online. - **Turn online/offline manually** or **Set to follow your schedule**. - **Visible online to visitor or customer** — Toggle visibility. - **Response Rate** — Choose expected reply time: **A few minutes**, **A few hours**, or **A few days**. - **Timezone** — Example: (GMT+08:00) Ulaanbaatar Standard Time. - **Display operator timezone** — Show operator's timezone. - **Hide messenger during offline hours** — Hide widget when offline. Click **Next step**. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/60668e23-0be1-4df9-7015-329a7923ff00/public) ### Step 5 — Settings General messenger settings. - **Default Language** — Example: English. - **Require authentication** — Ask visitors for email and phone. - **Show chat** — Show or hide the chat section. - **Show launcher** — Show or hide the chat button. - **Force logout** — End session when operator resolves conversation. - **Notify customer** — Send email when operator responds to offline customer. - **Show video call request** — Allow video call requests. Click **Next step**. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f5d45544-4426-437d-a6c4-3fb47b049100/public) ### Step 6 — Config Final integration setup. - **Name** — Example: test - **Brand** — Select the brand for this integration. Optional sections you can expand: - **Bot Setup** - **Ticket Config** - **Knowledge Base Topic** Click **Save** to finish. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4e1e1e63-dd58-4b1c-66e4-78ceb3f8c500/public) ## **Summary** The **Channels** section is where you manage all customer communication channels. You can: 1. Create and name channels. 2. Add team members. 3. Build ticket pipelines. 4. Create forms to collect customer data. 5. Create surveys to ask questions. 6. Save response templates for fast replies. 7. Connect integrations like Messenger, Facebook, Instagram, Discord, Call, and Email. --- # [Notification] Published 2026-09-22 > Settings → Notifications The Notifications page lets you control which events and plugins can send you notifications. You can turn notifications on or off for each plugin and choose how you want to receive them. How to o ## **Settings → Notifications** The **Notifications** page lets you control which events and plugins can send you notifications. You can turn notifications on or off for each plugin and choose how you want to receive them. ## **How to open Notifications** 1. Click **Settings** in the bottom-left sidebar. 2. Under the **ACCOUNT** section, click **Notification**. 3. The Notifications panel opens on the right. On the left side, you will see a list of plugins grouped by category: - **CORE PLUGIN** - **OPERATION PLUGIN** - **SALES PLUGIN** - **FRONTLINE PLUGIN** Each plugin has its own notification settings. Click any plugin name to see its events. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ee76cb79-4a04-46c9-0e81-ba298019e600/public) ## **1\. Core Plugin — Structure** - **Path:** Settings → Notification → Structure - **URL:** /settings/notification/core:structure ### What it does This controls notifications about changes to the company structure, such as branches, departments, and positions. ### What is there At the top: - **Core** toggle — Turn this on to receive any notifications from the Core plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Structure** toggle — Turn on/off all Structure notifications at once. Shows 0 of 3 events active. ### Events - **Branch assignee changed** Triggered when the assignee of a branch is changed. - **Department assignee changed** Triggered when the assignee of a department is changed. - **Position assignee changed** Triggered when the assignee of a position is changed. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ee76cb79-4a04-46c9-0e81-ba298019e600/public) ## **2\. Operation Plugin — Task** - **Path:** Settings → Notification → Task - **URL:** /settings/notification/operation:task ### What it does This controls task-related notifications. ### What is there At the top: - **Operation** toggle — Turn this on to receive any notifications from the Operation plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Task Notifications** toggle — Turn on/off all Task notifications at once. Shows 0 of 2 events active. ### Events - **Task Assigned** Triggered when a user is assigned to a task. - **Task Status changed** Triggered when a task status is changed. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bbe52061-e177-4044-f9b3-f73861204400/public) ## **3\. Operation Plugin — Project** - **Path:** Settings → Notification → Project - **URL:** /settings/notification/operation:project ### What it does This controls project-related notifications. ### What is there At the top: - **Operation** toggle — Turn this on to receive any notifications from the Operation plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Project Notifications** toggle — Turn on/off all Project notifications at once. Shows 0 of 2 events active. ### Events - **Project Assigned** Triggered when a user is assigned to a project. - **Project Status changed** Triggered when a project status is changed. - ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/147b1bfa-dc1c-47ae-0348-8bb852d1a600/public) ## **4\. Sales Plugin — Deals** - **Path:** Settings → Notification → Deals - **URL:** /settings/notification/sales:deals ### What it does This controls deal/sales-related notifications. ### What is there At the top: - **Sales** toggle — Turn this on to receive any notifications from the Sales plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Deals** toggle — Turn on/off all Deals notifications at once. Shows 0 of 2 events active. ### Events - **Deal assignee** Triggered when a user is assigned to a deal. - **Deal status changed** Triggered when a deal status is changed. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/dc565b85-5697-4027-ff29-cec3ebff0700/public) ## **5\. Sales Plugin — Note** - **Path:** Settings → Notification → Note - **URL:** /settings/notification/sales:note ### What it does This controls note-related notifications in the Sales plugin. ### What is there At the top: - **Sales** toggle — Turn this on to receive any notifications from the Sales plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Note** toggle — Turn on/off all Note notifications at once. Shows 0 of 1 events active. ### Events - **Mentioned in note** Triggered when a user is mentioned in a note. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/02ba9b24-5a4d-49dc-c7c4-3e55fed28e00/public) ## **6\. Frontline Plugin — Conversations** - **Path:** Settings → Notification → Conversations - **URL:** /settings/notification/frontline:conversations ### What it does This controls notifications for customer or team conversations. ### What is there At the top: - **Frontline** toggle — Turn this on to receive any notifications from the Frontline plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Conversations** toggle — Turn on/off all Conversation notifications at once. Shows 0 of 6 events active. ### Events - **Message added** Triggered when a message is added to a conversation. - **Assignee changed** Triggered when a conversation assignee is changed. - **Conversation created** Triggered when a conversation is created. - **Participant added** Triggered when a participant is added to a conversation. - **State changed** Triggered when a conversation state is changed. - **Conversation tagged** Triggered when a conversation is tagged. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/421539f6-c8eb-453b-9f21-5decbbf80a00/public) ## **7\. Frontline Plugin — Channels** - **Path:** Settings → Notification → Channels - **URL:** /settings/notification/frontline:channels ### What it does This controls channel-related notifications in the Frontline plugin. ### What is there At the top: - **Frontline** toggle — Turn this on to receive any notifications from the Frontline plugin. - Notification channels: **Email**, **Mobile soon**, **Other soon**. Below that: - **Channels** toggle — Turn on/off all Channel notifications at once. Shows 0 of 1 events active. ### Events - **Assignee change** Triggered when a channel member is changed. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7f961025-ae36-4730-a46b-3b7cee459d00/public) --- # [Profile] Published 2026-09-22 > Settings → Profile Path: Settings → Profile URL: /settings/profile Simple explanation The Profile page is where you manage your personal account information, profile picture, contact details, and social links. What is th ## **Settings → Profile** - **Path:** Settings → Profile - **URL:** /settings/profile ### Simple explanation The **Profile** page is where you manage your personal account information, profile picture, contact details, and social links. ### What is there #### Profile picture - **Upload** — Click to add or change your profile picture. - **Remove** — Click to delete your current profile picture. - Helper text: _"Upload a profile picture to help identify you."_ #### Name - **Username** — Your account username. - **Firstname** — Your first name. - **Lastname** — Your last name. #### Email - **Email** — Your public email address. #### More Information This section contains extra personal information such as: - **Position** — Your job position. Click _Add Positions_ to add. - **Middle Name** - **Short Name** - **Employee ID** - **Phone (Operator)** — Your phone number with country code. - **Join Date** — When you joined the company. - **Birth Date** — Your date of birth. Helper text: _"Provide any relevant additional personal information, if applicable."_ #### Link - This is your public link address. - You can add links to your social profiles such as Facebook, X, Website, etc. After making changes, click **Update** to save. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5372ae79-d3fc-4982-9fdf-6ebb96d26800/public) --- # [Change Password] Published 2026-09-22 > How to change your password in erxes Next (SaaS) Step 1: Open Settings Click Settings at the bottom of the left sidebar. Step 2: Select Change Password Go to Settings → ACCOUNT → Change Password . Step 3: Fill in the fie ## **How to change your password in erxes Next (SaaS)** ### Step 1: Open Settings Click **Settings** at the bottom of the left sidebar. ### Step 2: Select Change Password Go to **Settings → ACCOUNT → Change Password**. ### Step 3: Fill in the fields **CURRENT PASSWORD** \[your current password\] **NEW PASSWORD** \[your new password\] **RE-ENTER YOUR PASSWORD TO CONFIRM** \[your new password again\] ### Step 4: Check the password rules Your new password must meet all of these rules: - At least 8 characters - At least 1 number - At least 1 lowercase letter - At least 1 uppercase letter - Passwords must match A valid example format: Erxes2026! ### Step 5: Save After filling in all fields, click the **Save** button at the bottom. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/be78dbde-38ce-422a-9a09-a11f64e46900/public) --- # AI Automation Creation Guide Published 2026-09-22 > AI Automation Creation Guide A complete guide to creating an AI Agent with Kimi for Code in erxes, connecting Facebook/Instagram Messenger bots, and building an automation that sends automatic replies when messages arriv ## **AI Automation Creation Guide** A complete guide to creating an AI Agent with Kimi for Code in erxes, connecting Facebook/Instagram Messenger bots, and building an automation that sends automatic replies when messages arrive. **1** ## **Create an AI Agent** First, create the AI Agent that will be used in the automation. This agent receives user messages and prepares replies based on predefined knowledge and responsibilities. ### **Step 1: Settings → Automations → Agents** 1. 1\. Log in to the erxes system. 2. 2\. From the left menu, go to **Settings** → **Automations**. 3. 3\. Click the **Agents** tab. 4. 4\. In Provider, select **Kimi for Code**. 5. 5\. Click **Create Agents** or **Create First Agent**.   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d17efdf2-c96e-4b55-912c-b3b7e978eb00/public) Go to Settings → Automations → Agents, select Kimi for Code, then click Create Agents. ### **Step 2: Enter General Information** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c27bbd8e-b99c-4420-0ed0-73c3af905000/public) Fill in Name and Description in the General tab. **Field** **Value** **Example** **Name** AI Agent name Delgerekh **Description** Description Delgerekh's AI agent ### **Step 3: Connect Kimi Coding** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3dd8cabb-b7b7-47b3-b694-34aa45115200/public) In the Connection tab, configure Provider, Model, API key, Temperature, Max Tokens, and Timeout. ### **Connection Settings** 1. **1\. Provider:** Kimi Coding 2. **2\. Model:** KIMI-FOR-CODING 3. **3\. API Key:** Enter the API key purchased from Kimi Code 4. **4\. Base URL:** https://api.kimi.com/coding 5. **5\. Temperature:** 0.2 6. **6\. Max Tokens:** 500 7. **7\. Timeout:** 15000 ms ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/447d1a27-782c-44e4-5b10-95f25219cb00/public) To get a Kimi Code API key, choose a paid plan at www.kimi.com/code. **Note: An API key is required to use Kimi Coding. If you don't have one, register on the official Kimi Code website and choose the appropriate plan to get an API key.** ### **Step 4: Write the Context / System Prompt** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/524b9463-3b63-4a6a-c51a-c875534c3000/public) In the Context tab, write the AI assistant's role and company information. ### **System Prompt Example** **\# You are Delgerekh's official AI assistant** **\## Your main responsibility** **Receive customer questions, suggestions, requests, and complaints related to delivery, provide accurate information, and escalate to the relevant staff when necessary.** **\---** **\## About the company** **\[Write detailed company information, services, products, and contact details here** ### **Step 5: Save the AI Agent** After filling in all the information, click **Save**. The created agent appears in the AI Agents list. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/811c7fa2-cd24-4aa4-44e6-575d005f2c00/public) The Delgerekh agent appears in the AI Agents list. **2** ## **Connect Messenger Bots** Before using the AI Agent in an automation, you need to connect Facebook Messenger and Instagram Messenger bots. ### **Step 1: Go to Automations → Bots** 1. 1\. Go to **Settings** → **Automations** → **Bots**. 2. 2\. Facebook Messenger and Instagram Messenger bots are shown here. 3. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4ccd2975-c133-454f-ff77-4ab5be81fb00/public) The Automation bots section shows Facebook Messenger and Instagram Messenger. 1. ### **Step 2: Connect Facebook Messenger Bot** 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9cc3550b-3fde-4835-ba0b-28e329a99b00/public) Select the Facebook account. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/517e9063-32e1-478a-44dd-8314e69ddd00/public) Select the Facebook Page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1121b6d3-56e0-4fbb-5d5b-3ca3f3f0e900/public) Configure the bot name (Delgerekh AI) and persistent menu.   ### **Steps to Connect Facebook Messenger Bot** 1. **1\. Click Add Bot on the Facebook Messenger card.** 2. **2\. STEP 1: Select the Facebook account to connect (example: Bat-Erdene Bayartsogt).** 3. **3\. STEP 2: Select the Facebook Page to connect (example: Mongol Tech Hub).** 4. **4\. Enter the bot name (example: Delgerekh AI).** 5. **5\. Add a Persistent menu if needed.** 6. **6\. Click Save.** ### **Step 3: Connect Instagram Messenger Bot** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/392066ce-9569-4355-e265-b4dd50335000/public) Select the Instagram account. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5d48c4b7-6d6c-4e79-f862-3dd204e32600/public) Select the Instagram Page. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/98f807ce-75ff-48be-e104-7c8e637d5d00/public) Configure the Instagram bot name (Delgerekh Instagram AI) and Get Started menu. ### **Steps to Connect Instagram Messenger Bot** 1. **1\. Click Add Bot on the Instagram Messenger card.** 2. **2\. STEP 1: Select the Instagram account to connect (example: Bat-Erdene Bayartsogt).** 3. **3\. STEP 2: Select the Instagram Page to connect (example: uu759204).** 4. **4\. Enter the bot name (example: Delgerekh Instagram AI).** 5. **5\. Configure Get Started or other persistent menus.** 6. **6\. Click Save.** **3** ## **Create the Automation** Once the AI Agent and bots are ready, create an automation so the AI replies automatically when a user sends a message. ### **Step 1: Go to Automations and Create a New Automation** 1. 1\. From the main left menu, go to **Core modules** → **Automations**. 2. 2\. Click **Create a new automation**. 3. 3\. Enter the automation name (example: delgerekh ai chatbot). 4. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e71a5e09-75ee-43d7-d3ad-579a0cbd9c00/public) Core modules → Automations → Create a new automation. 1. ### **Step 2: Configure the Trigger** 2. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/69b484d4-0cc2-49f6-85f2-c1acbef1dd00/public) Trigger type: Facebook Message, Bot: DELGEREKH AI, Event: Direct Message. 1. ### **Trigger Configuration** 2. **1\. In the workflow, click + Add trigger in the Start → Trigger section.** 3. **2\. Select trigger type: Facebook Message.** 4. **3\. Select bot: DELGEREKH AI.** 5. **4\. Select event: Direct Message (when a user sends a direct message).** 6. **5\. Click Save Configuration.** **Step 3: Add the AI Agent Action** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/de62df2d-e40d-4bee-bc9b-b2f6c117ac00/public) AI Agent action Configuration tab: AI Agent = Delgerekh, Goal Type = Generate Text. ### **AI Agent Action Configuration** 1. **1\. After the trigger, click + to add an Action.** 2. **2\. Select action type: AI Agent.** 3. **3\. Configuration tab:** - **\-AI Agent: Delgerekh (the created agent)** - **\-Goal Type: Generate Text** - **\-AI Input: leave blank to send the entire trigger payload** - **\-Instruction Prompt: add extra instructions if needed** - **\-Fallback Response: message shown if the reply is delayed** ### **Step 4: Configure AI Agent Memory** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/be538c61-054f-4c14-b878-1d1d0516c500/public) Memory tab: Read/Write namespace is {{ trigger.conversationId }}. ### **Memory Configuration** 1. **1\. In the AI Agent action, open the Memory tab.** 2. **2\. Read from memory: enabled** 3. **3\. Read namespace: {{ trigger.conversationId }}** 4. **4\. Save result to memory: enabled** 5. **5\. Write namespace: {{ trigger.conversationId }}** 6.  **6. Click Save Configuration.** **Important:** Using {{ trigger.conversationId }} allows each user to have their own conversation history stored. This lets the AI agent remember previous messages and provide better replies. ### **Step 5: Add the Send Facebook Message Action** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/263ab860-0a92-4b49-bb26-8ee819c26500/public) In the Send Facebook Message action, set Text to {{prevAction.text}}. ### **Reply Action** 1. 1\. After the AI Agent action, click **+** again to add another Action. 2. 2\. Select action type: **Send Facebook Message**. 3. 3\. Click **Add Text** in the message sequence. 4. 4\. In the Text field, enter: {{prevAction.text}}. 5. 5\. Click **Save Configuration**. **4** ## **Save, Activate, and Verify the Automation** After configuring all triggers and actions, save and activate the automation. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a34814ea-bbbd-4526-1547-781e4b3b2200/public) Complete workflow: Trigger → AI Agent → Send Facebook Message. 1. **1\. Check the workflow** Trigger: Facebook Message → DELGEREKH AI → Direct Message; Action 1: AI Agent (Generate text, reads memory); Action 2: Send Facebook Message ({{prevAction.text}}) **2\. Save** Click the **Save** button in the top-right corner. **3\. Activate** Toggle INACTIVE to active to switch the automation to **active** status. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f1e506c1-0395-4908-fef6-87fc04081b00/public) The delgerekh ai chatbot automation appears as active in the Automations list. Name **VISIBILITY** **STATUS** **TRIGGERS** **ACTIONS** delgerekh ai chatbot Public **active** 1 2 ### **Test the Automation** 1. 1\. Send a test message from the Facebook Page Messenger. 2. 2\. If the automation is configured correctly, the AI agent's reply will arrive within a few seconds. 3. 3\. If no reply arrives, check the logs in Automation → History. **Troubleshooting:** - \*Check that the AI Agent's API key is valid. - \*Check that the bot is connected correctly. - \*Check that the automation is active. - \*Check that Direct Message is selected in the trigger. - \*Check that {{prevAction.text}} is entered correctly in the Send Facebook Message action. --- # Tags Published 2026-09-22 > How to use: Access tag management by selecting "Tags" from the WORKSPACE section in the settings sidebar. Browse tag categories in the left panel organized by section: CORE TAGS (Workspace, Customer, Company, Product & S **How to use:** Access tag management by selecting "Tags" from the WORKSPACE section in the settings sidebar. Browse tag categories in the left panel organized by section: CORE TAGS (Workspace, Customer, Company, Product & Service), FRONTLINE TAGS (Inbox, Ticket, Form), OPERATION TAGS (Task, Project), and SALES TAGS. Click on any tag category to view and manage tags within that group—each category organizes tags for specific parts of your system. Add new tag groups by clicking "Add Group" to create custom categories beyond the default ones. Create individual tags within a category by clicking the "+ Add Tag" button—enter tag name, choose a color, and optionally add a description. Select tag colors that create visual distinction in your system—use consistent color coding across related tags. Write clear, descriptive tag names that team members will understand and use consistently. Add tag descriptions to explain when and how each tag should be used—helps maintain tagging consistency across the team. Organize related tags together in appropriate categories—keep customer tags separate from product tags for easier management. Edit existing tags by clicking on them and updating the name, color, or description as needed. Delete unused tags by selecting them and removing—clean up your tag library periodically to avoid clutter. Create tag hierarchies if your system supports parent-child relationships—useful for nested categorizations like location hierarchies. Use tags consistently across your team by documenting tagging guidelines and training staff on proper usage. --- # System Logs Published 2026-09-22 > How to use: Access system logs by selecting "System Logs" from the DEVELOPER section in the settings sidebar. Use the "Filter" button immediately—logs contain extensive records and filtering is essential for finding rele **How to use:** Access system logs by selecting "System Logs" from the DEVELOPER section in the settings sidebar. Use the "Filter" button immediately—logs contain extensive records and filtering is essential for finding relevant information. Filter by date range to focus on specific time periods—select start and end dates for targeted investigation. Filter by status to show only successful operations or only errors—quickly identify problems or confirm successful actions. Filter by action type to see specific activities—creates, updates, deletes, logins, configuration changes, or other operations. Filter by source to view logs from specific system components—database, email, integrations, or particular modules. Filter by user to track individual team member actions—useful for accountability, training, or security audits. Review filtered log entries to understand system activity—each shows timing, action type, source, and responsible user or process. Investigate errors by filtering for failed operations—examine details to understand what went wrong and when it occurred. Track user activity for security purposes—monitor login attempts, data access, permission changes, and sensitive operations. Monitor system health by reviewing operation patterns—frequent errors may indicate configuration or infrastructure problems. Export filtered logs for compliance reporting—download specific timeframes or activity types for regulatory requirements. Use logs for troubleshooting reported issues—check what happened in the system around the time problems occurred. Identify automated processes by filtering for system-generated actions—understand background jobs and scheduled tasks. Compare activity across different periods to establish normal system behavior and detect anomalies. Share relevant log entries with technical support when requesting help—provides valuable context about system behavior. --- # 2. How to Connect Facebook Messenger Published 2026-09-22 > Description: Connecting your Facebook Messenger allows you to receive and reply to customer messages directly inside Frontline . Each Messenger integration must be linked to a specific Channel . Steps to Connect Facebook **Description:** Connecting your **Facebook Messenger** allows you to receive and reply to customer messages directly inside **Frontline**. Each Messenger integration must be linked to a specific **Channel**. ### **Steps to Connect Facebook Messenger** 1. Go to your **Channel**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/23bacff9-51be-4368-190d-5078ea281500/public) 2. Select **Facebook Messenger** from the list of integrations   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/433bba4a-4423-48cd-6412-77349a3bfc00/public) 3. Click **\+ Add Facebook Messenger Integration**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/c5929bd7-92b7-4c1d-c4f8-15797c983a00/public) 4. Click **Connect Facebook Account**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/68b20b50-ae8a-4a83-1bdc-0dcc39093500/public) 5. Select your **Facebook Profile Name**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3abbfc2b-449f-4fa4-892e-a3a885e61200/public) 6. Click **Next**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a369c584-458e-40be-3675-6cd2f0950e00/public) 7. Choose the **Facebook Page** you want to connect   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7dd3593c-0512-4fd6-d537-901db1a8bd00/public) 8. Click **Next** again   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/d998eb84-f560-43ae-d1ef-2e3cb0e51500/public) 9. Enter a name for your integration (for example, _Gaming Facebook Messenger_)   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f020eeb2-4b8c-4af7-30d4-b94a73dfc200/public) 10. Click **Save** to complete the setup   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/5a47542a-ea7b-4107-71fb-c5794f3ceb00/public) ### **💡Tips** - Make sure you’re logged in to the **correct Facebook account** before connecting. - If your organization manages multiple Facebook Pages, connect each one to a separate Channel for better tracking and performance insights. --- # [Email template] Create, Edit, Delete, Duplicate Published 2026-02-24 > Hello, you can create, delete, and edit email templates by following these instructions. Create a new email template: Go to Settings Click on the email template. Click on New email template. Give a name and enter your te Hello, you can create, delete, and edit email templates by following these instructions.  - Create a new email template: 1. Go to **Settings** 2. Click on **the email template.** 3. Click on **New email template.** 4. **Give a name** and enter your texts. 5. Click the **Save button**   - Edit email template: 1. Go to **Settings** 2. Click on **the email template.** 3. **Go to Edit** and edit. 4. Click the **Save button**   - Delete email template: 1. Go to **Settings** 2. Click on **the email template.** 3. **Press the Delete** button - Duplicate email template: 1. Go to **Settings** 2. Click on **the email template.** 3. Click the Duplicate button to copy. --- # [Email template] Use on Teaminbox Published 2026-02-24 > Hello, you can use the Email template in Team Inbox by following the instructions below. Go to Team Inbox Click on Conversation Go to the Contact section. Click on the Email Icon A small field will appear with the option Hello, you can use the Email template in Team Inbox by following the instructions below.  - Go to **Team Inbox** - Click on **Conversation** - Go to **the Contact section.** - Click on **the Email Icon** - **A small field** will appear with the option **to send an email.** - **Click on Email template** to select from the prepared email templates and send. --- # [Email template] Using IMAP Published 2026-02-24 > Hello, you can use your Email template on IMAP by following the instructions below. Go to Team Inbox Click on a conversation that came in via IMAP connection. Click Reply . Click on Email template to select from the prep Hello, you can use your Email template on IMAP by following the instructions below.  1. Go to **Team Inbox** 2. Click on a conversation that came in via **IMAP connection.** 3. Click **Reply .** 4. **Click on Email template** to select from the prepared email templates and send. --- # [Email template] Use in Customer emails Published 2026-02-24 > Hello, you can use your Email Template when sending emails to Users by following these instructions. Go to Contact Select your customer and click enter. Click on the email icon. A small field will appear with the option Hello, you can use your Email Template when sending emails to Users by following these instructions.  1. Go to **Contact** 2. **Select your customer** and click enter. 3. Click on **the email icon.** 4. **A small field** will appear with the option **to send an email.** 5. **Click on Email template** to select from the prepared email templates and send. --- # [Email template] Use in Broadcast Published 2026-02-24 > Hello, you can use your Email Template when sending mass emails by following the instructions below. Go to Broadcast Click on New broadcast. Enter a title. Select Email and click Next. Who is this broadcast for? хэсэгт C Hello, you can use your Email Template when sending mass emails by following the instructions below.  1. Go to **Broadcast** 2. Click on **New broadcast.** 3. Enter **a title.** 4. **Select Email** and click Next. 5. **Who is this broadcast for?** хэсэгт - **Choose a message type:** Select the type of email you want to send (Segment, Tag, Brand) - **Segment type:** If Segment type is selected, select the module in which the Segment is located. - **Choose your segment:** Select the users with the segment you want to send to. 6. You can choose and customize your email template in **Compose your broadcast.** - **From** : Choose which email address to send the email from. - **Sender** : Enter who you are sending the email from. - **Reply to:** Sets which email address the reply will go to. - **Email Subject:** Enter the general content of the email. - **Email Template** : Select your email template. - **Attachments:** Attach a file - **Sent to the following email as test** : A section where you can test your email. 7. **Full Preview:** See the email template as it is being sent. --- # [Email template] Use in a form Published 2026-02-24 > Hello, you can use your Email Template to automatically send emails to users when they fill out a form by following these instructions. Go to Forms Click Edit. Go to the Confirmation section. Click on the On page. Select Hello, you can use your Email Template to automatically send emails to users when they fill out a form by following these instructions. 1. Go to **Forms** 2. Click **Edit.** 3. Go to **the Confirmation section.** 4. Click on **the On page.** 5. Select **Email** 1. **Send email** : Choose which email to send from 2. **Subject** : Enter the general content of the email. 3. **Email template** : Choose a pre-made email, or select any email template and replace the text content within it. 6. Click the **Save button.** Also, if you want to set up an automatic email notification to yourself or another admin email address whenever a user fills out a form, click the checkbox below the Confirmation field. 1. **Admin emails** Enter the email address to which you want to receive form completion notifications. 2. **Subject Line** Enter the Subject value of the email. 3. **Message** description Enter a value. For emails to be sent automatically, only one of the following conditions must be met: - Your form must have a field to capture the user's email. If this field is not present, the above settings section will not be visible. --- # [Import] Deal Published 2026-02-24 > Hello, you can import Sales Field information or sales pipeline (deal) by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Deal by clicking Next. Click on Download Hello, you can import Sales Field information or sales pipeline (deal) by following the instructions below.  1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Deal** by clicking Next. 4. **Click on Download** template to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Choose a file to upload your cards:deal** and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save **the Disclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Task Published 2026-02-24 > Hello, you can import the Main Activity Field information or Tasks according to the following instructions. Go to the Settings menu. Go to Import & Export and select Import. Enter the Task by clicking the Next button. Cl Hello, you can import the Main Activity Field information or Tasks according to the following instructions.   1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. Enter **the Task by clicking the Next button.** 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Enter the Choose a file to upload your cards: task** and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Ticket Published 2026-02-24 > Hello, you can import the Complaint field information or Ticket by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Ticket by clicking Next. Click on Download temp Hello, you can import the Complaint field information or Ticket by following the instructions below.   1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Ticket** by clicking Next. 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Choose a file to upload your cards:** Enter ticket and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Customer Published 2026-02-24 > Hello, you can import Customer information registered in the Erxis system by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Customer and click Next. Click on Dow Hello, you can import Customer information registered in the Erxis system by following the instructions below.  1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Customer** and click Next. 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Choose a file to upload your cards: customer** and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Leads Published 2026-02-24 > Hello, you can import your registered Leads into the Erxis system by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Leads by clicking Next. Click on Download tem Hello, you can import your registered Leads into the Erxis system by following the instructions below.   1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Leads** by clicking Next. 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Choose a file to upload your cards:leads** and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Companies Published 2026-02-24 > Hello, you can import Company information registered in the Erxis system by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Companies and click Next. Click on Dow Hello, you can import Company information registered in the Erxis system by following the instructions below.   1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Companies** and click Next. 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Enter Choose a file to upload your cards:companies** and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Product & Service Published 2026-02-24 > Hello, you can import your registered Product and Service information into the Erxis system by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Product & Service a Hello, you can import your registered Product and Service information into the Erxis system by following the instructions below.  1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Product & Service** and click Next. 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Choose a file to upload your cards:** Enter product and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Import] Team member Published 2026-02-24 > Hello, you can import Teammembers (Employee) information registered in the Erxis system by following the instructions below. Go to the Settings menu. Go to Import & Export and select Import. Go to Teammember and click Ne Hello, you can import Teammembers (Employee) information registered in the Erxis system by following the instructions below.   1. Go to the **Settings menu.** 2. **Go to Import & Export** and select Import. 3. **Go to Teammember** and click Next. 4. **Click on Download template** to download the Excel template and enter your information. 5. Download as **a CSV file** 6. **Choose a file to upload your cards: teammember** and click Next. 7. **In the Select Property** section, match the header column with the erxis header information and give it a Name. 8. Check and save the **CheckDisclaimer .** 9. Check the results to see if the import was successful in the **See Records result section.** --- # [Export] Deal Published 2026-02-24 > Hello, you can export a sales pipeline (deal) by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Deal and then click Next. Select the fields in the Excel colu Hello, you can export a sales pipeline (deal) by following the instructions below.  1. Go to the **Settings menu.** 2. Click on **Import & Export.** 3. Select **Export .** 4. **Click on Deal** and then click Next. 5. Select the fields in the Excel column section of **Properties to export and click Next.** 6. After filtering the field with the values ​​you need, go to **the Filter section.** 7. Click **Apply Filter.** 8. Click **Count to count again.** 9. **Click Apply Filter,** click Next, and fill in the Name field. 10. **Accept the Disclaimer** and click Export. From the results that appear 11. Click on **Download Result to get your data.** --- # [Export] Task Published 2026-02-24 > Hello, you can export the Main Action Area or Task by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Task and then click Next. Select the fields in the Excel Hello, you can export the Main Action Area or Task by following the instructions below.  1. Go to the **Settings menu.** 2. Click on **Import & Export.** 3. Select **Export .** 4. **Click on Task** and then click Next. 5. Select the fields in the Excel column section of **Properties to export and click Next.** 6. After filtering the field with the values ​​you need, go to **the Filter section.** 7. Click **Apply Filter.** 8. Click **Count to count again.** 9. **Click Apply Filter,** click Next, and fill in the Name field. 10. **Accept the Disclaimer** and click Export. From the results that appear 11. Click on **Download Result to get your data.** --- # [Export] Ticket Published 2026-02-24 > Hello, you can export your Ticket by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Ticket and then click Next. Select the fields in the Excel column section Hello, you can export your Ticket by following the instructions below.  1. Go to the **Settings menu.** 2. Click on **Import & Export.** 3. Select **Export .** 4. **Click on Ticket** and then click Next. 5. Select the fields in the Excel column section of **Properties to export and click Next.** 6. After filtering the field with the values ​​you need, go to **the Filter section.** 7. Click **Apply Filter.** 8. Click **Count to count again.** 9. Click **Apply Filter.** 10. Click the **Next button and fill in the Name field.** 11. **Accept the Disclaimer** and click Export. From the results that appear 12. Click on **Download Result to get your data.** --- # [Export] Customer Published 2026-02-24 > Hello, you can export Customer information registered in the Erxis system by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Customer and then click Next. Sel Hello, you can export Customer information registered in the Erxis system by following the instructions below.   1. Go to the **Settings menu.** 2. Click on **Import & Export.** 3. Select **Export .** 4. **Click on Customer** and then click Next. 5. Select the fields in the Excel column section of **Properties to export and click Next.** 6. After filtering the field with the values ​​you need, go to **the Filter section.** 7. Click **Apply Filter.** 8. Click **Count to count again.** 9. Click **Apply Filter.** 10. Click the **Next button and fill in the Name field.** 11. **Accept the Disclaimer** and click Export. From the results that appear 12. Click on **Download Result to get your data.** --- # [Export] Leads Published 2026-02-24 > Hello, you can export Leads registered in the Erxis system by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Leads and then click Next. Select the fields in Hello, you can export Leads registered in the Erxis system by following the instructions below.  1. Go to the **Settings menu.** 2. Click on **Import & Export.** 3. Select **Export .** 4. **Click on Leads** and then click Next. 5. Select the fields in the Excel column section of **Properties to export and click Next.** 6. After filtering the field with the values ​​you need, go to **the Filter section.** 7. Click **Apply Filter.** 8. Click **Count to count again.** 9. Click **Apply Filter.** 10. Click the **Next button and fill in the Name field.** 11. **Disclaimer** accepted. 12. **Click Export** . From the results that appear, 13. Click on **Download Result to get your data.** --- # [Export] Product & Service Published 2026-02-24 > Hello, you can export Product & Services information registered in the Erxis system by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Product & Services and Hello, you can export Product & Services information registered in the Erxis system by following the instructions below.    - Go to the **Settings menu.** - Click on **Import & Export.** - Select **Export .** - **Click on Product & Services** and then click Next. - Select the fields in the Excel column section of **Properties to export and click Next.** - After filtering the field with the values ​​you need, go to **the Filter section.** - Click **Apply Filter.** - Click **Count to count again.** - Click **Apply Filter.** - Click the **Next button and fill in the Name field.** - **Accept the Disclaimer** and click Export. From the results that appear - Click on **Download Result to get your data.** --- # [Export] Team member Published 2026-02-24 > Hello, you can export Teammembers registered in the Erxis system by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Team Members and then click Next. Select t Hello, you can export Teammembers registered in the Erxis system by following the instructions below.    - Go to the **Settings menu.** - Click on **Import & Export.** - Select **Export .** - **Click on Team Members** and then click Next. - Select the fields in the Excel column section of **Properties to export and click Next.** - After filtering the field with the values ​​you need, go to **the Filter section.** - Click **Apply Filter.** - Click **Count to count again.** - Click **Apply Filter.** - Click the **Next button and fill in the Name field.** - **Accept the Disclaimer** and click Export. From the results that appear - Click on **Download Result to get your data.** --- # [Export] Conversation (Teaminbox) Published 2026-02-24 > Hello, you can export Teaminbox (Conversation) information registered in the Erxis system by following the instructions below. Go to the Settings menu. Click on Import & Export. Select Export . Click on Conversation and Hello, you can export Teaminbox (Conversation) information registered in the Erxis system by following the instructions below.  - Go to the **Settings menu.** - Click on **Import & Export.** - Select **Export .** - **Click on Conversation** and then click Next. - Select the fields in the Excel column section of **Properties to export and click Next.** - After filtering the field with the values ​​you need, go to **the Filter section.** - Click **Apply Filter.** - Click **Count to count again.** - Click **Apply Filter.** - Click the **Next button and fill in the Name field.** - **Accept the Disclaimer** and click Export. From the results that appear - Click on **Download Result to get your data.** --- # [Department] create, edit, delete Published 2026-02-24 > [Department] create, edit, delete Written by Amura Munguntsetseg 0 Modified atJun 10 2024 Hello, you can create a department by following the instructions below. Create a Department: Go to Settings Select Structure . Sel #### \[Department\] create, edit, delete ![Amura Munguntsetseg](https://erxes.s3.amazonaws.com/undefined0.9850277198819735Amarzaya.jpg) Written by Amura Munguntsetseg 0 Modified atJun 10 2024 Hello, you can create a department by following the instructions below.  - Create a Department: - Go to **Settings** - Select **Structure .** - Select **Departments** - Click on the **Add Department button and fill in the required fields below.** - **Title** : Provide the name of the team or department. - **Code** : Enter the team or department code. - **Team Members** : Including team and department staff - Click the **Save button.** - Edit Department: - Go to **Settings** - Select **Structure .** - Select **Department** - **Name** Select your department name. - Click the **Edit button to make your edits.** - Click the **Save button.** - Delete Department: - Go to **Settings** - Select **Structure .** - Select **Department** - **Name** Select your department name. - Press the **Delete button to delete** --- # [Branch] create, edit, delete Published 2026-02-24 > Hello, you can assign your team member to a branch by following the instructions below. Create a new branch: Go to Settings Select Structure . Select Branches Click the Add Branch button and fill in the required fields b Hello, you can assign your team member to a branch by following the instructions below.  - Create a new branch: - Go to **Settings** - Select **Structure .** - Select **Branches** - Click the **Add Branch button and fill in the required fields below.** - **Title** : Branch name - **Address** : Branch address - **Code** : Industry code - **Team Members** : Industry staff - **Longitude** : Google map location longitude - **Latitude** : Google map location latitude - **Radius** : Enter the number of meters from the Google map location within which you can register your time.  /For example: 500/ - Click the **Save button.**   - Edit Branch: - Go to **Settings** - Select **Structure .** - Select **Branches** - **Name** Select your branch name - Click the **Edit button to make your edits.** - Click the **Save button.** - Delete branch: - Go to **Settings** - Select **Structure .** - Select **Branches** - **Name** Select your branch name - Press the **Delete button to delete.** --- # [Product & Service] Insight (Deal) Published 2026-02-24 > Hello, you can use the Insight plugin by following the instructions below. Go to Insight Select Dashboard and click Add. Select all Deal options. Click the Save button. Click Add Chart . Select Service : Cards Chart Temp Hello, you can use the Insight plugin by following the instructions below.  1. Go to **Insight** 2. **Select Dashboard** and click Add. 3. **Select all Deal** options.  4. Click the **Save button.** 5. Click **Add Chart .** - **Select Service** : Cards - **Chart Template** : Total Deals Count select - **Select Dimension** : Select Product - **Select Measure** : Choose either Count or Amount. - Click the **Save button.** --- # [Product & Service] View archived categories Product Category Status Published 2026-02-24 > Hello, you can view the Product Category Status of the archived category by following the instructions below. Go to Settings Click on Product & Service The Filter category by Status section will appear, and the status of Hello, you can view the Product Category Status of the archived category by following the instructions below.  1. Go to **Settings** 2. Click on **Product & Service** 3. **The Filter category by Status** section will appear, and the status of that section will be displayed. 4. You can view archived categories by selecting **Disabled** or **Archived . To restore a category, click Edit.** 5. Set **the State** to **Active and then click OK.** 6. **When you click Save** , the category will be restored. --- # [Product & Service] View deleted items Product status delete Published 2026-02-24 > Hello, you can follow the instructions below to view deleted products Product status delete possible to do. Go to Settings If you enter Product & Service, it will be on the left. The Filter product by Status section will Hello, you can follow the instructions below to view deleted products Product status delete possible to do.  1. Go to **Settings** 2. If you enter **Product & Service, it will be on the left.** 3. **The Filter product by Status** section will appear, and the status of that section will be displayed. 4. **You can view deleted items by selecting the Deleted** option. To restore a deleted item: 5. Click on **Edit.** 6. **Clicking the Save** button will restore the item. --- # [Product & Service] Add a picture and description Published 2026-02-24 > Hello, you can add images and descriptions to the Product and Service information registered in the Erxis system by following the instructions below. Go to Settings If you enter Product & Service, the product names and t Hello, you can add images and descriptions to the Product and Service information registered in the Erxis system by following the instructions below. - Go to **Settings** - **If you enter Product & Service,** the product names and types will appear. - **Select your product** and go to edit. - Description in **the Description section** - You can add images to **the Featured image section.** --- # [Product & Service] Create, Delete, Edit Published 2026-02-24 > Hello, you can create, delete, and edit Products and Services by following the instructions below. Create a new product: Go to the Settings menu. Go to Product & Service Select Category Click Add Items. Code : Enter the Hello, you can create, delete, and edit Products and Services by following the instructions below. - Create a new product: 1. Go to the **Settings menu.** 2. Go to **Product & Service** 3. Select **Category** 4. Click **Add Items.** 1. **Code** : Enter the product code. 2. **Name** : Enter the product name. 3. **Type**: Product 4. **Description** : Enter a product description. 5. **Unit Price** : Enter the product price. 6. **Featured Image** : Insert a featured image of the product 7. **Secondary Image** : Add another image of the product 8. **UOM** : Enter product unit of measure. Watch the instruction video: [_https://www.loom.com/share/7e2ed834e68c4cd08d485f60a0b2c609?sid=e5f4bd2b-14a9-412e-a674-16b26595c30a_](https://www.loom.com/share/7e2ed834e68c4cd08d485f60a0b2c609?sid=e5f4bd2b-14a9-412e-a674-16b26595c30a) - Product repair: 1. Go to the **Settings menu.** 2. Go to **Product & Service** 3. Click on **Search.** 4. Search **by product name or code** 5. Select the product and click on **the pencil icon (edit) to make your edits.** 6. Click **Save** to save . Watch the instructional video: [_https://www.loom.com/share/416d8976c1024e1b9b3267aea12c3a16?sid=bfa46d5b-580e-4ee0-8026-764053355283_](https://www.loom.com/share/416d8976c1024e1b9b3267aea12c3a16?sid=bfa46d5b-580e-4ee0-8026-764053355283) - Product deletion: 1. Go to the **Settings menu.** 2. Go to **Product & Service** 3. Click on **Search.** 4. Search **by product name or code** 5. Select **your product** and click **the X.** 6. **Click Yes, I am** to save. Watch the instructional video: [_https://www.loom.com/share/6a770bf8033d4a7980988c8091df8e32?sid=7e4582c0-c273-431a-adb1-2857ef10e327_](https://www.loom.com/share/6a770bf8033d4a7980988c8091df8e32?sid=7e4582c0-c273-431a-adb1-2857ef10e327) --- # [Product & Service] Category Create, Delete, Edit Published 2026-02-24 > Hello, you can create, delete, and edit Product and Service Categories by following the instructions below. Create a new product category: Go to the Settings menu. Go to Product & Service Click on Add category. Code : En Hello, you can create, delete, and edit Product and Service Categories by following the instructions below. - Create a new product category: 1. Go to the **Settings menu.** 2. Go to **Product & Service** 3. Click on **Add category.** 1. **Code** : Enter a category code. 2. **Name** : Enter a category name. 3. **Description** : Enter a category description. 4. **State**: Active 5. **Image** : Insert category image 6. **Parent Category** : If a category has been created previously, it will be taken into account.   - Edit product category: 1. Go to the **Settings menu.** 2. Go to **Product & Service** 3. Select a category and click on **the pencil icon (edit) to make your edits.** 4. Click **Save to save.**   - Delete product category: 1. Go to the **Settings menu.** 2. Go to **Product & Service** 3. Select **a category** and click **the X icon.** 4. **Click Yes, I am** to save. --- # [System Config] Language Published 2026-02-24 > Hello, you can change the language used in the Erxis application by following the instructions below. Go to Settings Select System configuration . Click on General settings. Language : Click on English and select your de Hello, you can change the language used in the Erxis application by following the instructions below.  - Go to **Settings** - Select **System configuration .** - Click on **General settings.** - **Language** : Click on English and select your desired language. - Click the **Save button.** --- # [System Config] Currency Published 2026-02-24 > Hello, you can set the product currency to be used in the Erxis application by following the instructions below. Go to Settings Select System configuration . Click on General settings. Currency : Select the currency you Hello, you can set the product currency to be used in the Erxis application by following the instructions below.  - Go to **Settings** - Select **System configuration .** - Click on **General settings.** - **Currency** : Select the currency you want to use and select your language. - Click the **Save button.** --- # [System Config] With team member restrictions Published 2026-02-24 > Hello, You can set up a setting where only people in a team, department, or industry can mention and assign each other by following the instructions below. Go to Settings Select System configuration . Click on General se Hello, You can set up a setting where only people in a team, department, or industry can mention and assign each other by following the instructions below.  - Go to **Settings** - Select **System configuration .** - Click on **General settings.** - **With team member restrictions** : Check the box. - Click the **Save button.** --- # [System Config] Theme Published 2026-02-24 > Hello, You can change your Erxis system to your company's brand colors by following the instructions below. Go to Settings Select System configuration . Click on Theme Logo : Insert your organization's logo. Favicon : In Hello, You can change your Erxis system to your company's brand colors by following the instructions below.  - Go to **Settings** - Select **System configuration .** - Click on **Theme** - **Logo** : Insert your organization's logo. - **Favicon** : Insert the organization's logo - **Text color** : Enter the text color for the login page. - **Background** : Enter the color of the login page. - **Motto** : Include a large word at the top of the login page (Vision, etc.) - **Login page description** : Enter a short description at the top of the login page (Mission, etc.) - Click the **Save button.** --- # [System Config] Common mail config Published 2026-02-24 > Hello, You can follow the instructions below to configure which email address to send Notification Email, Team Member Invitation Email, and Forgot Password Email. Go to Settings Select System configuration . Click on Com Hello, You can follow the instructions below to configure which email address to send Notification Email, Team Member Invitation Email, and Forgot Password Email.  - Go to **Settings** - Select **System configuration .** - Click on **Common mail config** - **Email** : Enter the email address to send the information to. - **Type** : Select AWS SES or Custom mail server. - **Default email service** : Select AWS SES or Custom mail service. - Click the **Save button.** --- # [System Config] Constant Published 2026-02-24 > Hello, you can configure the User Gender, Social Address field, and Organization Industry options in the Contact module by following the instructions below. Go to Settings Select System configuration . Click on Constant Hello, you can configure the User Gender, Social Address field, and Organization Industry options in the Contact module by following the instructions below.  - Go to **Settings** - Select **System configuration .** - Click on **Constant** - **Pronoun choices** : User's Gender Choices - **Company Industry types :** Selection of the company's industry - **Social links :** Social links field - Click the **Save button.** --- # [System Config] AWS S3 Published 2026-02-24 > Hello, you can set up AWS S3 by following these instructions. Go to https://aws.amazon.com/ and create an account. Connect card After logging in, click on AWS Management console. Generate your AWS Secret key and Access k Hello, you can set up AWS S3 by following these instructions.  1. Go to [**https://aws.amazon.com/ and create an account.**](https://aws.amazon.com/) 2. Connect **card** 3. After logging in, click on **AWS Management console.** 4. Generate your **AWS Secret key and Access key in My security credentials.** 5. Go to **the AWS S3 section** 6. Click on **Create bucket.** 7. **Fill in your Bucket name** (all lowercase) and leave the rest as is. 8. Click **Create bucket.** 9. **In the Permissions section of the bucket** , edit the Block public access (bucket settings) section and uncheck the Block all public access section. 10. Generate and enter your policy in **the bucket policy .** --- # [Permissions] Create, edit, delete, search Published 2026-02-24 > Hello, you can create, delete, and edit Permissions settings by following the instructions below. Create a Permissions Group Go to Settings Select Permissions . Click Create user group, fill in the fields below and save. Hello, you can create, delete, and edit Permissions settings by following the instructions below.  - Create a Permissions Group 1. Go to **Settings** 2. Select **Permissions .** 3. Click **Create user group, fill in the fields below and save.** - **Name** : Give a name - **Description** : Write a description (optional) - **Members** : Select the members of the group for the given permissions setting. 4. Click the **Save button to save.**   - Create a new permission setting 1. Go to **Settings** 2. Select **Permissions .** 3. Enter **New permission and fill in the fields with a red asterisk.** - Choose the Module: Choose the module to authorize - Choose the Actions: Choose the actions to take - Choose the Groups or Team Member: Choose either the Group or Team Member - Allow: Check and grant the permission ( if unchecked, the permission is removed from the group )   - Edit permission settings 1. Go to **Settings** 2. **Go to the Permissions** section and find the permission setting you want to edit. 3. **You can edit it by clicking the Edit** button.   - Delete permission settings: 1. Go to **Settings** 2. **Go to the Permissions** section and select the deletion permission settings, then click on the 3. **Click the Remove** button and type Delete to delete it. --- # [Internal notes] Conversation Published 2026-02-24 > Hello, you can use Internal notes by following the instructions below. Go to Teaminbox Select Conversation (Write a note) 🔲 Click on the internal note icon and the conversation section will turn yellow, allowing you to Hello, you can use Internal notes by following the instructions below.  - Go to **Teaminbox** - Select **Conversation (Write a note)** - **🔲** Click on the internal note icon and the conversation section will turn yellow, allowing you to write a note in that section. - **Click Send** to send it to the user. Notes written here will not be sent to the user. - In the New notes section of the other fields, type your notes directly and click the save button. --- # [Internal notes] Task Published 2026-02-24 > Hello, you can use Internal notes in Tasks by following these instructions. Go to Task Select your Card (Write a note) Write your notes directly in the New notes section. Click the Save button to save. Hello, you can use Internal notes in Tasks by following these instructions.  - Go to **Task** - Select your **Card (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Ticket Published 2026-02-24 > Hello, you can use Internal notes on Tickets by following these instructions. Go to Ticket Select your Card (Write a note) Write your notes directly in the New notes section. Click the Save button to save. Hello, you can use Internal notes on Tickets by following these instructions.  - Go to **Ticket** - Select your **Card (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Sales pipeline Published 2026-02-24 > Hello, you can use Internal notes in the Sales pipeline by following these instructions. Enter the sales pipeline Select your Card (Write a note) Write your notes directly in the New notes section. Click the Save button Hello, you can use Internal notes in the Sales pipeline by following these instructions.  - Enter **the sales pipeline** - Select your **Card (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Customer Published 2026-02-24 > Hello, you can use Internal notes in Customer by following these instructions. Go to Contact Click on Customer Select Customer (Write a note) Write your notes directly in the New notes section. Click the Save button to s Hello, you can use Internal notes in Customer by following these instructions.  - Go to **Contact** - Click on **Customer** - Select **Customer (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Leads Published 2026-02-24 > Hello, you can use Internal notes in Leads by following these instructions. Go to Contact Click on Leads Select your Lead (Write a note) Write your notes directly in the New notes section. Click the Save button to save. Hello, you can use Internal notes in Leads by following these instructions.  - Go to **Contact** - Click on **Leads** - Select your **Lead (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Companies Published 2026-02-24 > Hello, you can use Internal notes in Company by following the instructions below. Go to Contact Click on Companies Select your Company (Write a note) Write your notes directly in the New notes section. Click the Save but Hello, you can use Internal notes in Company by following the instructions below.  - Go to **Contact** - Click on **Companies** - Select your **Company (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Team Members Published 2026-02-24 > Hello, you can use Internal notes in Team Member by following the instructions below. Go to Settings Click on Team Members Select your Member (Write a note) Write your notes directly in the New notes section. Click the S Hello, you can use Internal notes in Team Member by following the instructions below.  - Go to **Settings** - Click on **Team Members** - Select your **Member (Write a note)** - Write your notes directly in the **New notes section.** - Click the **Save button to save.** --- # [Internal notes] Automations Published 2026-02-24 > Hello, you can use Internal notes in Automations by following these instructions. Go to Automation Select Automation (Write a note) 🗒 Write your notes directly in the New notes section Click the Save button to save. Hello, you can use Internal notes in Automations by following these instructions.  - Go to **Automation** - Select **Automation (Write a note)** - **🗒** Write your notes directly in the New notes section - Click the **Save button to save.** --- # [Broadcast] Sent mass email Published 2026-02-24 > Hello, you can send a Mass Email by following the instructions below. Go to Broadcast Click on New broadcast. Enter a title. Select Email and click Next. Who is this broadcast for? хэсэгт Choose a message type: Select th Hello, you can send a Mass Email by following the instructions below.  1. Go to **Broadcast** 2. Click on **New broadcast.** 3. Enter **a title.** 4. **Select Email** and click Next. 5. **Who is this broadcast for?** хэсэгт - **Choose a message type:** Select the type of email you want to send (Segment, Tag, Brand) - **Segment type:** If Segment type is selected, select the module in which the Segment is located. - **Choose your segment:** Select the users with the segment you want to send to. 6. You can choose and customize your email template in **Compose your broadcast.** - **From** : Choose which email address to send the email from. - **Sender** : Enter who you are sending the email from. - **Reply to:** Sets which email address the reply will go to. - **Email Subject:** Enter the general content of the email. - **Email Template** : Select your email template. - **Attachments:** Attach a file - **Sent to the following email as test** : A section where you can test your email. 7. **Full Preview:** See the email template as it is being sent. 8. **Save & Draft** : Save and exit 9. **Send & Live** : Send your mass emails --- # [Broadcast] Send mass notifications to mobile applications Published 2026-02-24 > Hello, you can send mass notifications to people using your organization's mobile app by following these instructions. Go to Broadcast Click on New broadcast. Enter a title. Select Notification and click Next. Select the Hello, you can send mass notifications to people using your organization's mobile app by following these instructions.  1. Go to **Broadcast** 2. Click on **New broadcast.** 3. Enter **a title.** 4. **Select Notification** and click Next. 5. **Select the type of business portal** : Select Client Portal from the drop-down menu. 6. **Choose a Client Portal** : Choose a name 7. **Choose a Message Type** : Select the type of notification you want to send (Segment, Tag, Brand). 8. **Segment Type** : Select the user type and Segment to send notifications to. 9. **Compose your notification** Enter the content of the notification to send. - **Choose your notification type** Choose your notification type - In-app push notification - Mobile & Web push notification - **Notification Title** Enter the notification title. - **Notification Content** Enter notification content 10. **Save & Draft** : Save and exit 11. **Send & Live** : Send your mass emails --- # [Broadcast] Send mass messages using Business messenger Published 2026-02-24 > Hello, you can send mass messages to people who visit your organization's website by following the instructions below. Go to Broadcast Click on New broadcast. Enter a title. Select Messenger and click Next. Choose a Mess Hello, you can send mass messages to people who visit your organization's website by following the instructions below.  1. Go to **Broadcast** 2. Click on **New broadcast.** 3. Enter **a title.** 4. **Select Messenger** and click Next. 5. **Choose a Message Type** : Select the type of message you want to send (Segment, Tag, Brand). 6. **Segment Type** : Select the user type and Segment to send the message to. 7. Click **Next, Next.** 8. **Message:** Enter the message content to send. 9. Choose who to send **from .** 10. Choose which brand to send **to** 11. **Message type** Select your message type. 1. Chat 2. Note 3. Post 12. Select the type of message you want to send **as Sent as** 1. Badge 2. Snipped 3. Show the full message 13. **Save & Draft** : Save and exit 14. **Send & Live** : Send your mass emails --- # [System Logs] Published 2026-02-24 > System Logs System Logs --- # [Client portal] Published 2026-02-24 > Client portal [Client portal](https://officenext.erxes.io/settings/client-portals) --- # [Mail config] Published 2026-02-24 > Mail config [Mail config](https://officenext.erxes.io/settings/mail-config) --- # [File upload] Published 2026-02-24 > File upload [File upload](https://officenext.erxes.io/settings/file-upload) --- # [Bundle Rule] Published 2026-02-24 > Bundle Rule [Bundle Rule](https://officenext.erxes.io/settings/products/bundle-rule) --- # [Product Rule] Published 2026-02-24 > Product Rule [Product Rule](https://officenext.erxes.io/settings/products/product-rule) --- # [Bundle Condition] Published 2026-02-24 > Bundle Condition [Bundle Condition](https://officenext.erxes.io/settings/products/bundle-condition) --- # [Similarity Group] Published 2026-02-24 > Similarity Group [Similarity Group](https://officenext.erxes.io/settings/products/similarity-group) --- # [General config] Published 2026-02-24 > General config [General config](https://officenext.erxes.io/settings/products/general-config) --- # [Categories] Published 2026-02-24 > Categories [Categories](https://officenext.erxes.io/settings/products/categories) --- # [UOM] Published 2026-02-24 > UOM [UOM](https://officenext.erxes.io/settings/products/uoms) --- # [Product Service] Published 2026-02-24 > Product Service [Product Service](https://officenext.erxes.io/settings/products) --- # [Brands] Filter Team Inbox and Contacts by brand Published 2026-02-24 > Hello, you can sort your Team inbox by brand by following the instructions below. Go to the team inbox. Click on the ☰ symbol Click on Brands You can select the Brand you want to distinguish and see only the chats that c Hello, you can sort your Team inbox by brand by following the instructions below. 1. Go to **the team inbox.** 2. Click on the [☰ symbol](https://coolsymbol.com/copy/Trigram_for_Heaven_Symbol_%E2%98%B0) 3. Click on **Brands** 4. You can select the Brand you want to distinguish and see only the chats that come from that channel. You can sort Contacts by brand by following these instructions. 1. Go to **Contacts** 2. Click on the [☰ symbol](https://coolsymbol.com/copy/Trigram_for_Heaven_Symbol_%E2%98%B0) 3. Click on **Brands** 4. You can select the brand you want to differentiate and see the users and customers generated from that brand. --- # [Brands] Create, Edit, Delete Published 2026-02-24 > Hello, you can create, delete, and edit Brands by following these instructions. Create a new brand: Go to Settings Click on Brands Click Add New Brand . Give your name . Click the Save button. Brand editing: Go to Settin Hello, you can create, delete, and edit Brands by following these instructions.  - Create a new brand: 1. Go to **Settings** 2. Click on **Brands** 3. Click **Add New Brand .** 4. Give your name **.** 5. Click the **Save button.**   - Brand editing: 1. Go to **Settings** 2. Click on **Brands** 3. **Name** Choose your brand name 4. Click the **Edit button to make your edits.** 5. Click the **Save button.**   - Delete a brand: 1. Go to **Settings** 2. Click on **Brands** 3. **Name** Choose your brand name 4. Press the **Delete button.** 5. **Clicking Yes, I am** will successfully delete it. --- # [Brands] About Published 2026-02-24 > We consider this concept of a brand as a distinct entity within an organization, but with many separate operations, sales channels, and customers. For example : We have several different sales channels, such as Erhes (ma We consider this concept of a brand as a distinct entity within an organization, but with many separate operations, sales channels, and customers. _For example_ : _We have several different sales channels, such as Erhes (marketing software), Erhet (financial software), Gerege Agent (a team that makes websites and mobile apps), and we have tried to separate the parts with their respective users using the concept of a brand._ This way, the following small logics work on the body: - Only one knowledgebase applies to one brand. - Only one web messenger per brand - Classify the user by the brand they visited. - They classify team members based on which brand they belong to. --- # [Properties] Task Published 2026-02-24 > Hello, you can add a new field to the Main Action field or Task by following the instructions below. Add a field to a Task: Go to Settings Select Properties Select Tasks . Click Add Group & Field. Select Add Group and fi Hello, you can add a new field to the Main Action field or Task by following the instructions below.  - Add a field to a Task:  1. Go to **Settings** 2. Select **Properties** 3. Select **Tasks .** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Boards & Pipelines**_ : Select the Boards & pipelines in the Task pane you created. You can select multiple Boards & pipelines here. If you don't select them, you will encounter the problem of not seeing the Properties you created. - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. **Show in card** : The information will be visible outside the Properties task card you created. This will be the same as the information that appears outside the card when you select Company or Customer. 10. Click the **Save button to save.**   - Edit the Task field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Tasks .** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Task field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Tasks .** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Properties] Ticket Published 2026-02-24 > Hello, you can add a new field to the Main Action field or Ticket by following the instructions below. Add a field to a ticket and create it: Go to Settings Select Properties Select Ticket Click Add Group & Field. Select Hello, you can add a new field to the Main Action field or Ticket by following the instructions below.  - Add a field to a ticket and create it:  1. Go to **Settings** 2. Select **Properties** 3. Select **Ticket** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Boards & Pipelines**_ : Select the Boards & pipelines for the Ticket field you created. You can select multiple Boards & pipelines here. If you do not select this, you will have the problem that the Properties you created will not be visible. - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. **Show in card** : The information will be visible outside the Properties task card you created. This will be the same as the information that appears outside the card when you select Company or Customer. 10. Click the **Save button to save.**   - Edit the Task field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Ticket** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Task field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Ticket** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Properties] Sales pipeline Published 2026-02-24 > Hello, you can add a new field to the Main Action field or Deal by following the instructions below. Add a field to a Deal and create: Go to Settings Select Properties Choose Deal Click Add Group & Field. Select Add Grou Hello, you can add a new field to the Main Action field or Deal by following the instructions below.  - Add a field to a Deal and create:  1. Go to **Settings** 2. Select **Properties** 3. Choose **Deal** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Boards & Pipelines**_ : Select the Boards & pipelines of the Deal field you created. You can select multiple Boards & pipelines here. If you do not select this, you will encounter the problem of not seeing the Properties you created. - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. **Show in card** : The information will be visible outside the Properties task card you created. This will be the same as the information that appears outside the card when you select Company or Customer. 10. Click the **Save button to save.**   - Edit the Deal field:  1. Go to **Settings** 2. Select **Properties** 3. Choose **Deal** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Deal field:  1. Go to **Settings** 2. Select **Properties** 3. Choose **Deal** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Properties] Customer Published 2026-02-24 > Hello, you can add a new field to the Main Action field, Customer, by following the instructions below. Add a field to Customer: Go to Settings Select Properties Select Customer Click Add Group & Field. Select Add Group Hello, you can add a new field to the Main Action field, Customer, by following the instructions below.  - Add a field to Customer:  1. Go to **Settings** 2. Select **Properties** 3. Select **Customer** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. Click the **Save button to save.**   - Edit the Deal field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Customer** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Customer field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Customer** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Properties] Companies Published 2026-02-24 > Hello, you can add a new field to the Main Activity field, Companies, by following the instructions below. Add a field to Companies and create: Go to Settings Select Properties Select Companies Click Add Group & Field. S Hello, you can add a new field to the Main Activity field, Companies, by following the instructions below.  - Add a field to Companies and create:  1. Go to **Settings** 2. Select **Properties** 3. Select **Companies** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. Click the **Save button to save.**   - Edit the Companies field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Companies** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Companies field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Companies** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Properties] Team member Published 2026-02-24 > Hello, you can add a new field to the Main Action field, Teammember, by following the instructions below. Add a field to Teammember: Go to Settings Select Properties Select Teammember Click Add Group & Field. Select Add Hello, you can add a new field to the Main Action field, Teammember, by following the instructions below.  - Add a field to Teammember:  1. Go to **Settings** 2. Select **Properties** 3. Select **Teammember** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. Click the **Save button to save.**   - Edit the Teammember field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Teammember** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Teammember field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Teammember** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Properties] Product & Service Published 2026-02-24 > Hello, you can add a new field to the Main Activity field, Product & Service, by following the instructions below. Add a field to Product & Service: Go to Settings Select Properties Select Product & Service Click Add Gro Hello, you can add a new field to the Main Activity field, Product & Service, by following the instructions below.  - Add a field to Product & Service:  1. Go to **Settings** 2. Select **Properties** 3. Select **Product & Service** 4. Click  **Add Group & Field.** 5. **Select Add Group** and fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Always open**_ : Option to keep the field open whenever you enter the task card. 6. Click the **Save button to save.** 7. Click **Add Group & Field** again. 8. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  9. Click the **Save button to save.**   - Edit the Product & Service field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Product & Service** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   - Delete the Product & Service field:  1. Go to **Settings** 2. Select **Properties** 3. Select **Product & Service** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # [Tag] Create, Edit, Delete Published 2026-02-24 > Hello, you can create, edit, and delete tags or categories by following the instructions below. Create a new Tag: Go to Settings Select Tags Select the plugin you want to create a tag from. Click +Add tag and fill in the Hello, you can create, edit, and delete tags or categories by following the instructions below. - Create a new Tag: 1. Go to **Settings** 2. Select **Tags** 3. Select the **plugin you want to create a tag from.** 4. **Click +Add tag** and fill in the fields that appear. 5. Click the  **Save button.** - Edit tag: 1. Go to **Settings** 2. Select **Tags** 3. Select **Plugin -aa** 4. Click on **the pen icon (edit)** next to the Edit Tag name. 5. Click the **Save button to save.** - Delete tag: 1. Go to **Settings** 2. Select **Tags** 3. Select **Plugin -aa** 4. Delete Click on **the X** next to the Tag name. 5. **Click Yes, I am** and save. --- # [Tag] About Published 2026-02-24 > A tag is a large classification of entities used to classify, identify, and then report information. A tag is considered a LABEL for a card only. A tag is a large classification of entities used to classify, identify, and then report information. A tag is considered a LABEL for a card only. --- # [General] Published 2026-02-24 > About General About General --- # [Team Member] Invite a team member Published 2026-02-24 > Hello, you can add your Employee by following the instructions below. Go to Settings Click on Team member Fill in the information in the Invite team member field and save. Email address - email address of the inviting us Hello, you can add your Employee by following the instructions below. - Go to **Settings** - Click on **Team member** - **Fill in the information in the Invite team member** field and save. - **Email address** - email address of the inviting user  - **Password** - create a password  - **Permission** - select access rights Watch the instructional video: [_https://www.loom.com/share/202148b23cda4193b491ba70fb65610a?sid=6224e1c5-d5b9-49b3-a8e1-7ef20e538ecd_](https://www.loom.com/share/202148b23cda4193b491ba70fb65610a?sid=6224e1c5-d5b9-49b3-a8e1-7ef20e538ecd) --- # [Team Member] Deactive (Delete) Published 2026-02-24 > Hello, you can disable Team Member access to Erxis by following the instructions below. Go to Settings Select Team Members . Choose Team Member: Select the team member to deactivate. Status : Deactive Worth knowing: The Hello, you can disable Team Member access to Erxis by following the instructions below.  - Go to **Settings** - Select **Team Members .** - **Choose Team Member:** Select the team member to deactivate. - **Status** : Deactive   Worth knowing: The Erxis system has a built-in logic that somehow removes Team Members, and the basic logic is to simply deactivate them. --- # [Team Member] Assign to Branch Published 2026-02-24 > Hello, you can assign your team member to a branch by following the instructions below. Go to Settings Select Structure . Select Branches Click on the Edit button. In the Team Members section, select and enter your team Hello, you can assign your team member to a branch by following the instructions below.  - Go to **Settings** - Select **Structure .** - Select **Branches** - Click on the **Edit button.** - **In the Team Members** section, select and enter your team members. - Click the **Save button.** --- # [Team Member] Assign to Department Published 2026-02-24 > Hello, you can assign a team member to a department by following the instructions below. Go to Settings Select Structure . Select Department Click on the Edit button. In the Team Members section, select and enter your te Hello, you can assign a team member to a department by following the instructions below.  - Go to **Settings** - Select **Structure .** - Select **Department** - Click on the **Edit button.** - **In the Team Members** section, select and enter your team members. - Click the **Save button.** --- # [Team Member] View team member number on the erxes XOS mobile app Published 2026-02-24 > Hello, you can easily view Team Member information on your mobile app by following the instructions below. Go to Play store / App store Search for erxes XOS and download it. Go to the app and click Sign-in. Enter your or Hello, you can easily view Team Member information on your mobile app by following the instructions below.  - Go to **Play store / App store** - Search for **erxes XOS and download it.** - Go to the app and click **Sign-in.** - Enter your organization's email address URL in **the Sign info section.** - If **it is Saas,** check Is Saas Server and click ✔️ to continue. If it is Open source,  **uncheck** Is Saas Server and click ✔️ to continue. - Enter  **your Email** and **Password .** - Click the **Connect button.** - Go to **Team Members** - Go to the selected **Team Member** 's **Profile** - **You can click the phone icon** to make a call or perform other actions. --- # Teams Published 2026-02-16 > How to use: Navigate to "Teams" in the OPERATION section of settings to manage your organizational structure. View all existing teams in the table showing TITLE (team names with icons), MEMBERS (number of team members), **How to use:** Navigate to "Teams" in the OPERATION section of settings to manage your organizational structure. View all existing teams in the table showing TITLE (team names with icons), MEMBERS (number of team members), TASKS (number of assigned tasks), and CREATED AT (when team was created). Create a new team by clicking the "+ Create team" button with keyboard shortcut (⌘C) in the top right corner—opens a form to set up your team. Enter a clear team name that identifies the department, function, or purpose—examples include "Development", "Support SaaS", "DevOps", "Marketing Team". Write a team description explaining the team's responsibilities, scope of work, and which areas they handle in the organization. Add team members by selecting employees from your user list—click to add multiple members who will be part of this team. Assign a team lead or manager by designating someone to oversee the team's work and performance—gives them additional permissions. Set team permissions controlling what the entire team can access—define which modules, features, and data they can view or edit. Configure team routing rules so work is automatically assigned to the appropriate team—tickets, deals, or tasks routed based on criteria like topic, location, or customer type. Create sub-teams or nested teams to reflect hierarchical structures—parent teams with specialized child teams underneath for complex organizations. Set team territories if working with geographic divisions—define which regions, accounts, or customer segments each team owns. Configure team-specific working hours if different from organization defaults—some teams may work different shifts or time zones. Set team colors for visual identification in calendars, reports, and dashboards—helps quickly identify which team owns what. Assign team goals and quotas for performance tracking—sales targets, ticket resolution goals, customer satisfaction scores. Click on any team name to view detailed information including full member list, assigned work, and team performance metrics. View team size in the MEMBERS column showing how many people belong to each team—ranges from 1 member to over 100 in larger teams. Check team workload in the TASKS column showing number of assigned tasks—helps identify overloaded teams or unbalanced distribution. Monitor when teams were created in the CREATED AT column—tracks team history and organizational evolution over time. Edit existing teams by clicking on the team row—update name, description, members, or settings as the organization changes. Remove members from teams while keeping them as system users—adjust team composition when roles or responsibilities shift. Archive inactive teams rather than deleting—preserves historical team structure and data without cluttering the active list. Balance workload by reviewing member and task counts across teams—redistribute work if some teams are overloaded. Create team-based reports to track performance metrics at team level—compare teams against each other or organizational goals. Set up team communication channels for internal collaboration—team-specific announcements, discussions, or shared resources. Transfer work between teams by bulk reassigning tickets, tasks, or deals when organizational responsibilities change. Use team structure for access control—grant permissions at team level rather than individual users for easier management. Review team composition regularly to ensure alignment with current organizational structure and business needs. Delete teams that are truly obsolete and no longer needed—only after confirming no critical data or assignments will be lost. --- # Change Password Published 2026-02-16 > How to use: Navigate to password settings by clicking your profile and selecting "Change Password" from the menu. Enter your current password in the first field to verify your identity before making changes. Type your ne **How to use:** Navigate to password settings by clicking your profile and selecting "Change Password" from the menu. Enter your current password in the first field to verify your identity before making changes. Type your new password in the "New Password" field—create a strong password following the security requirements shown. Re-enter the same new password in the confirmation field to prevent typing errors—both fields must match exactly. Review the password requirements checklist as you type: at least 8 characters, at least 1 number, at least 1 lowercase letter, at least 1 uppercase letter, and passwords must match. Use the eye icon next to each password field to temporarily show or hide what you're typing for verification. Choose a unique, strong password that combines letters, numbers, and symbols—avoid common words or personal information. Click "Save" or "Change Password" when all requirements are met—you may need to sign in again with your new password. Store your new password securely in a password manager rather than writing it down or reusing passwords from other accounts. --- # Invoices Published 2026-02-16 > How to use: Access invoice settings by opening "Invoices" under the PAYMENT section in settings. Design invoice templates by customizing layout, colors, and branding—add logo, company information, adjust positioning. Con **How to use:** Access invoice settings by opening "Invoices" under the PAYMENT section in settings. Design invoice templates by customizing layout, colors, and branding—add logo, company information, adjust positioning. Configure invoice numbering format using prefixes, sequential numbers, suffixes—create format like INV-2024-0001. Set starting invoice number if migrating from another system—begin at specific value rather than 1. Configure default due date for payment terms—due on receipt, net 15, net 30, net 60 days applied automatically. Set up automatic late fees applied when invoices unpaid past due—percentage or fixed amount with grace period. Create line item templates for commonly invoiced items—speeds invoice creation with pre-filled descriptions and prices. Configure tax calculation including single or multiple rates, compound taxes, or tax-exempt handling. Set up discount options: line-item discounts, subtotal discounts, or promotional code application. Define payment terms and conditions appearing on invoices—return policy, late fees, accepted methods. Configure invoice footer with banking details for wire transfers, check instructions, or legal disclaimers. Set up multi-currency invoicing for international clients—invoice in customer's local currency with conversion. Enable partial payment acceptance allowing customers to pay invoices in multiple installments. Configure invoice reminders sent automatically before and after due dates—customize frequency and messaging. Set up recurring invoices for subscriptions or regular services—define billing frequency and auto-generation. Create invoice approval workflows requiring manager review before sending—ensures accuracy. Configure credit note handling for refunds or adjustments—generate credit notes linked to original invoices. Set up invoice delivery: email automatically upon generation, manual download, or postal mail for traditional clients. Enable customer self-service portal where clients view invoices, payment history, and download copies. Configure invoice email templates with professional formatting and clear payment call-to-action. Set up invoice status tracking: draft, sent, viewed, partially paid, paid, overdue, written off. Define access permissions controlling who can create, view, edit, void, or delete invoices. Enable invoice attachments for supporting documents like timesheets or delivery receipts. Configure invoice analytics tracking total invoiced, outstanding receivables, average payment time, aging reports. Set up invoice sequences for different types or business units—separate numbering for divisions. Create deposit invoice templates for advance payments—link to final invoices upon completion. Configure write-off procedures for uncollectible invoices—requires approval and accounting integration. Set up invoice export formats compatible with accounting software—automate bookkeeping. Enable customer comments on invoices for questions or notes about specific charges. Configure multi-language invoices using customer's preferred language—useful for international business. Test invoice generation with samples—review formatting, calculations, and tax computations before sending to clients. --- # Payment Settings Published 2026-02-16 > How to use: Access payment settings by navigating to "Payment Settings" under the PAYMENT section in settings. Connect a payment gateway by clicking "Add Payment Gateway"—select your provider from available options. Ente **How to use:** Access payment settings by navigating to "Payment Settings" under the PAYMENT section in settings. Connect a payment gateway by clicking "Add Payment Gateway"—select your provider from available options. Enter payment gateway credentials including API keys, merchant IDs, and secret keys from your provider's dashboard. Configure supported payment methods: credit cards, debit cards, ACH transfers, digital wallets, buy-now-pay-later options. Set up credit card processing by enabling accepted card types—Visa, Mastercard, American Express, Discover. Configure payment page branding with your logo and colors—builds customer trust during checkout. Set default currency for transactions based on your primary business location—enable multi-currency if needed internationally. Enable currency conversion if accepting international payments—set exchange rates or use automatic updates. Configure payment processing fees—decide if absorbed by business or passed to customers as surcharges. Set up automatic payment receipts sent via email—customize receipt templates with your branding. Configure failed payment handling including retry attempts, customer notifications, and dunning for subscriptions. Set up refund policies and processing—full or partial refunds with approval workflows. Enable payment security features like 3D Secure authentication for additional fraud protection. Configure CVV requirements mandating security codes for card-not-present transactions. Set up address verification service checking billing addresses match cards—reduces fraud risk. Define payment capture timing—immediate capture or delayed capture after order fulfillment. Configure subscription billing for recurring payments—billing cycles, proration, failed payment retry logic. Set up installment plans allowing customers to pay over time—define terms, interest, schedules. Enable saved payment methods for returning customers—requires PCI compliance and tokenization. Configure payment tokenization keeping sensitive card data off your servers—improves security. Set up payment webhooks for real-time notifications—successful charges, refunds, disputes. Create payment forms and checkout pages with necessary billing and shipping fields. Configure payment notifications alerting your team about transactions, failures, or refunds. Set up dispute management for chargebacks—receive notifications and provide evidence. Define payment terms for invoices: net 30, net 60, due on receipt—different terms for customer types. Configure late payment fees automatically added when invoices overdue. Enable partial payments letting customers pay invoices in installments. Set up payment portal where customers view invoices and make self-service payments. Test payment processing in sandbox mode before going live—process test transactions to verify setup. --- # Deals Published 2026-02-16 > How to use: Access deal settings by navigating to "Deals" in the SALES section of the settings menu. View current deal stages in your sales pipeline showing progression from initial contact to closure. Create new deal st **How to use:** Access deal settings by navigating to "Deals" in the SALES section of the settings menu. View current deal stages in your sales pipeline showing progression from initial contact to closure. Create new deal stages by clicking "Add Stage"—name each step in your specific sales process. Arrange stage order by dragging and dropping—represent the typical flow of your sales cycle from first to last step. Set stage probabilities indicating likelihood of closing—helps with accurate revenue forecasting at each stage. Configure stage colors for visual distinction—use color progression showing advancement through the pipeline. Define stage requirements or exit criteria—what must happen before deals move to the next stage. Set up automatic actions when deals enter stages—trigger notifications, create tasks, or update fields automatically. Configure deal rotting or aging rules—identify deals stuck too long in stages without progress. Create multiple pipelines for different sales types—separate processes for new business versus renewals or different product lines. Assign pipelines to specific teams who work different sales processes—customized workflows for different selling motions. Set pipeline-specific fields appearing only for certain deal types—customize data capture for different sales scenarios. Configure deal closure reasons for lost deals—standardize tracking of why deals don't close for analysis. Set up required information for deal closure—ensure documentation of outcomes before marking complete. Define deal properties capturing important sales information—budget, decision criteria, competition, timeline. Configure deal value calculation rules—one-time revenue, recurring revenue, or lifetime value calculations. Set up deal forecasting methods—probability-based, weighted pipeline, or custom algorithms. Create deal templates for common sales—pre-filled information and steps for standard transactions. Configure deal assignment rules—distribute new deals among sales reps using round-robin, territory, or manual assignment. Set up deal access permissions—control who can view, edit, or delete deals for data security. Enable deal splitting for team selling—multiple reps share credit and commission on collaborative sales. Define deal activities for logging interactions—calls, meetings, emails automatically associated with deal records. Set minimum deal sizes or qualification thresholds for pipeline entry. Monitor deal aging to identify stalled opportunities requiring attention or different approach. --- # Teams Published 2026-02-16 > How to use: Navigate to "Teams" in the OPERATION section of settings to manage your organizational structure. Create a new team by clicking "Add Team" or "Create Team"—name it according to department, function, or locati **How to use:** Navigate to "Teams" in the OPERATION section of settings to manage your organizational structure. Create a new team by clicking "Add Team" or "Create Team"—name it according to department, function, or location. Write a team description explaining the team's purpose, responsibilities, and scope of work. Add team members by selecting employees from your user list—staff can belong to multiple teams as needed. Assign a team lead or manager by designating someone to oversee the team's activities and performance. Set team permissions controlling what the entire team can access—modules, features, and data visibility. Configure team routing rules for automatic work assignment—tickets, leads, or tasks routed to appropriate teams based on criteria. Create sub-teams or nested teams to reflect hierarchical organizational structure—parent teams with specialized child teams. Set team territories if working with geographic or account-based divisions—define ownership of regions or customer segments. Configure team-specific settings like working hours, holiday calendars, or SLA targets that differ from defaults. Assign team colors for visual identification in calendars, reports, and dashboards—helps quickly spot team assignments. Set team goals and quotas for performance tracking—sales targets, ticket resolution goals, or other KPIs. View team workload distribution to identify overloaded or underutilized staff—balance assignments across team members. Create team communication channels or discussion spaces for internal collaboration and announcements. Set up team schedules showing member availability, shifts, or rotation schedules for coverage planning. Configure team-based reporting to track metrics at team level—compare teams or monitor against goals. Assign shared resources to teams like email addresses, phone numbers, or equipment—clarify team assets. Remove members from teams while keeping them as system users—adjust team composition as roles change. Archive inactive teams to preserve historical structure without cluttering active team lists. Create team templates for quickly setting up new teams with standard structures and permissions. Transfer work between teams by bulk reassigning when responsibilities change organizationally. --- # Channels Published 2026-02-16 > How to use: Access channel configuration by selecting "Channels" from the FRONTLINE section in the settings sidebar. Create your first communication channel by clicking the "+ Create channel" button to begin setup. Choos **How to use:** Access channel configuration by selecting "Channels" from the FRONTLINE section in the settings sidebar. Create your first communication channel by clicking the "+ Create channel" button to begin setup. Choose the channel type you want to add—email, live chat, Facebook Messenger, WhatsApp, Instagram, SMS, or other platforms. Configure email channels by connecting email accounts through IMAP/SMTP or setting up forwarding to route messages into the system. Set up live chat by generating widget code and embedding on your website—customize appearance, position, and greeting messages. Connect social media channels by authenticating with platforms—grant permissions for pages to receive and send messages. Configure messaging apps like WhatsApp Business by providing API credentials and business phone numbers. Set up SMS channels by integrating with providers—configure phone numbers and messaging capabilities. Assign channels to specific teams or queues—route different channels to appropriate departments or specialists. Configure channel-specific settings like working hours, auto-responses, and away messages for each communication method. Customize greeting messages that customers see when first contacting you through each channel. Set up routing rules determining how incoming messages are distributed—round-robin, skill-based, or load-balanced. Enable or disable channels temporarily without deleting configuration—useful during maintenance or capacity adjustments. Test each channel after setup by sending test messages and verifying they appear correctly in your Frontline Inbox. Monitor channel connection status regularly—ensure all channels remain active and messages flow properly. Edit channel settings as needed—update credentials, change routing rules, or modify team assignments. Remove channels no longer in use—disconnect and delete to maintain clean configuration. Create multiple instances of the same channel type if needed—separate email addresses or phone numbers with different routing. --- # Client Portal Published 2026-02-16 > How to use: Access client portal management by selecting "Client Portal" from the DEVELOPER section in the settings sidebar. Create a new customer portal by clicking the "+ Create client portal" button—opens a setup form **How to use:** Access client portal management by selecting "Client Portal" from the DEVELOPER section in the settings sidebar. Create a new customer portal by clicking the "+ Create client portal" button—opens a setup form for configuration. Enter a descriptive portal name that identifies its purpose or target customer group clearly. Configure the portal domain where customers access their portal—set up subdomain or custom domain for professional branding. Customize portal branding by uploading your logo, selecting colors, and choosing themes to match company identity. Set up portal features—enable or disable modules like ticket viewing, invoice access, document downloads, knowledge base access. Configure registration settings—decide if customers can self-register or require invitation from your team. Define authentication requirements including password complexity, two-factor authentication, or single sign-on options. Set customer permissions controlling what actions they can perform—view-only, submit tickets, upload files, make payments. Generate portal access tokens for API integrations or programmatic access to portal features. Edit existing portals by clicking the three-dot menu—update settings, change branding, or modify permissions as needs evolve. Test portals thoroughly before launching—create test accounts and verify all features function correctly and branding displays properly. Share portal access with customers including URL, login instructions, and explanation of available features. Monitor portal usage and collect customer feedback to continuously improve the self-service experience. Delete outdated portals when no longer needed while preserving historical data for compliance. Create multiple portals for different customer segments if serving diverse groups with different needs. --- # Brands Published 2026-02-16 > How to use: Access brand management by selecting "Brands" from the WORKSPACE section in the settings sidebar. Create a new brand by clicking the "+ Create brand" button—opens a form to set up a distinct business identity **How to use:** Access brand management by selecting "Brands" from the WORKSPACE section in the settings sidebar. Create a new brand by clicking the "+ Create brand" button—opens a form to set up a distinct business identity. Enter a clear brand name that identifies the business unit, division, or sub-brand you're creating. Write an optional brand description explaining what this brand represents or which customers it serves. Note the automatically generated brand code—this short identifier is used throughout the system when selecting brands. Upload a brand-specific logo by clicking the logo upload area—each brand can have its own visual identity. Configure brand colors by setting primary and secondary colors—ensures consistent branding in customer communications. Set up brand-specific email settings including sender name, reply-to address, and email signature unique to this brand. Assign team members to specific brands so they work within the appropriate brand context. Create brand-specific templates for emails, documents, and forms that match each brand's voice and style. Use the three-dot menu next to each brand to edit settings, update branding elements, or manage brand assignments. Filter the brand list if you have many brands—search by name or code to find specific brands quickly. Archive brands that are no longer active rather than deleting—preserves historical data while removing from active use. Apply brands to customers, deals, and communications so each interaction reflects the appropriate brand identity. Review your brand structure periodically to ensure it still reflects your organizational structure and business needs. --- # Mail Config Published 2026-02-16 > How to use: Access email configuration by selecting "Mail Config" from the WORKSPACE section in the settings sidebar. Set the sender email address in the "EMAIL" field—this appears as the "from" address on all system-gen **How to use:** Access email configuration by selecting "Mail Config" from the WORKSPACE section in the settings sidebar. Set the sender email address in the "EMAIL" field—this appears as the "from" address on all system-generated emails. Select your email template style from the "TYPE" dropdown—choose "Simple" for default templates or "Custom" for customized designs. Configure your email service provider in the "DEFAULT EMAIL SERVICE" section—select your provider from the dropdown menu. Enter your email service credentials starting with the access key ID—obtain this from your email provider's dashboard. Provide the secret access key for authentication—keep this confidential and never share publicly. Set your service region—choose the geographic area where your email service is hosted. Enter the configuration set name if your email provider uses this for tracking and analytics. Click "Update" to save all email configuration changes—test email delivery afterward to verify settings work correctly. Ensure your email service account is properly set up and verified before configuring these settings—prevents delivery failures. Test email sending after configuration by triggering a system notification or password reset email. Monitor email delivery rates and bounce reports through your email service provider's dashboard. Update credentials immediately if they're compromised—rotate keys regularly for security best practices. Consider using separate email configurations for different environments—development, testing, and production systems. --- # File Upload Published 2026-02-16 > How to use: Access file upload configuration by selecting "File Upload" from the WORKSPACE section in the settings sidebar. Review the two sections: "UPLOAD FILE TYPES" for general system uploads and "UPLOAD FILE TYPES O **How to use:** Access file upload configuration by selecting "File Upload" from the WORKSPACE section in the settings sidebar. Review the two sections: "UPLOAD FILE TYPES" for general system uploads and "UPLOAD FILE TYPES OF WIDGET" for embedded upload features. View currently allowed file types displayed as removable tags—each shows the format description and file extension. Remove file types from the allowed list by clicking the "×" next to that format—blocks users from uploading those file types. Add new file types by using the search field—type format names or extensions to find and select additional formats to allow. Configure image format permissions—decide which image types (JPEG, PNG, GIF, SVG, WEBP, HEIC, HEIF) users can upload. Set document format restrictions—control uploads of Word, Excel, PowerPoint, PDF, and other office documents. Manage compressed file permissions—allow or block ZIP, RAR, 7z, and other archive formats. Control media uploads by setting which audio and video formats are acceptable—MP3, MP4, WAV, MPEG, etc. Configure widget-specific restrictions separately—embedded upload widgets may need tighter restrictions for security. Balance security with functionality—allow necessary formats while blocking potentially dangerous file types like executables. Review and update file type settings periodically as business needs change or security threats evolve. Test upload restrictions by attempting to upload various file types and verifying the system properly accepts or rejects them. Communicate file upload policies to users so they understand which formats are supported and why restrictions exist. Consider file size limits alongside file type restrictions for complete upload control—manage storage capacity effectively. --- # Integration Config Published 2026-02-16 > How to use: Access integrations by navigating to "Integrations Config" in the FRONTLINE section of settings. Browse available integrations in the marketplace—organized by category like CRM, accounting, marketing, e-comme **How to use:** Access integrations by navigating to "Integrations Config" in the FRONTLINE section of settings. Browse available integrations in the marketplace—organized by category like CRM, accounting, marketing, e-commerce, payment processors. Search for specific integrations by name if you know which application you want to connect. Click on any integration to view details including description, features, requirements, and setup instructions before connecting. Connect new integrations by clicking "Connect," "Install," or "Authorize"—follow the authentication process for that specific service. Enter API keys or credentials when required—copy these from the other application's settings into the integration form. Authorize integrations via OAuth by signing into the other service and approving requested permissions. Configure integration settings after connecting—map data fields between systems, set sync frequency, choose which data to share. Set up field mapping to ensure data translates correctly—match contact fields, deal stages, product information between systems. Configure sync direction: one-way (OfficeNext to app or app to OfficeNext) or two-way bidirectional synchronization. Set sync frequency determining update timing—real-time, hourly, daily, or manual only depending on needs. Enable selective sync by choosing which specific records or data types should transfer between systems. Test integrations after setup by creating test records and verifying they appear correctly in both systems. Monitor integration status through the dashboard—check last sync time, errors, and connection health regularly. View integration logs showing all data exchanges and any errors—troubleshoot sync problems or data discrepancies. Pause integrations temporarily without disconnecting—useful during testing, migrations, or system maintenance periods. Update integration settings as requirements change—adjust field mappings, sync frequency, or data selection criteria. Disconnect integrations no longer needed—choose whether to preserve or delete synced data from your system. Set up webhook integrations for custom applications not in the marketplace—configure endpoint URLs and authentication. Create integration-based automations triggering actions when data syncs—automated workflows based on external system events. Monitor API usage if integrations have call limits—avoid exceeding quotas that could break connections. Configure error notifications for integration failures—receive alerts when syncs fail for quick resolution. --- # POS Published 2026-02-16 > How to use: Access POS settings by opening the "POS" section under SALES in the settings menu. Register new POS terminals by adding each register or device—assign names, locations, and unique identifiers. Configure recei **How to use:** Access POS settings by opening the "POS" section under SALES in the settings menu. Register new POS terminals by adding each register or device—assign names, locations, and unique identifiers. Configure receipt templates controlling what appears on customer receipts—company info, policies, promotional messages. Set up accepted payment methods: cash, credit/debit cards, mobile payments, gift cards, store credit. Configure card reader integration by connecting to payment hardware—enter device IDs and pairing codes. Set up cash drawer settings including starting cash amounts and denominations used at your location. Configure tax calculation rules for transactions—rates by location, product type, or customer status. Set up discount capabilities available at POS—percentage or dollar discounts with authorization requirements. Define user permissions for POS operations—who can process returns, void transactions, or apply large discounts. Set up barcode scanning by connecting scanners—configure automatic product addition or search behavior. Configure inventory integration so transactions automatically update stock levels in real-time. Set up offline mode allowing POS operation during internet outages—transactions sync when connection restored. Create quick-add buttons for frequently sold items—speeds checkout for common purchases. Configure customer display settings if using second screen—show prices and totals to customers during transactions. Set up receipt printer preferences—number of copies, auto-print settings, paper size. Configure till management for opening and closing shifts—cash counting, variance reporting, deposit preparation. Set up reporting access determining who can view sales reports and cash drawer totals. Enable customer features like email receipts, loyalty lookups, or account payments. Configure return policies including time limits, condition requirements, receipt requirements. Set up split payment processing for multiple payment methods on single transactions. Configure gift card activation, balance checking, and redemption procedures. Set up employee discounts with authorization and maximum percentage limits. Enable transaction notes for cashiers to add comments or special instructions. Configure age verification prompts for restricted products—require ID check before sale. Set up integrated scales for items sold by weight—pricing per unit calculation. Enable quick cash functions for common amounts—preset buttons for frequent denominations. Test POS thoroughly with sample transactions before going live—verify payments, discounts, and integrations work correctly. --- # Products Published 2026-02-16 > How to use: Access product management by selecting "Products" from the WORKSPACE section in the settings sidebar. Switch between "Product" and "Product Service" tabs at the top to manage different product types. Browse p **How to use:** Access product management by selecting "Products" from the WORKSPACE section in the settings sidebar. Switch between "Product" and "Product Service" tabs at the top to manage different product types. Browse product organization options in the left panel: Product Service, Categories, UOM (Unit of Measure), General config, Similarity Group, Bundle Condition, Bundle Rule, and Product Rule. Add a new product by clicking the "+ Add Product" button—opens a form to create new product entries. Enter the product name that customers will see on invoices, quotes, and throughout the system. Write a product description explaining features, benefits, and specifications—this appears in customer-facing materials. Set the product SKU or code for internal tracking and inventory management—use consistent numbering systems. Configure pricing by entering the base price, cost, and any quantity-based or subscription pricing tiers. Select the product category to organize items in logical groups—helps with browsing and reporting. Choose product type: physical goods, services, digital products, or subscriptions—affects how they're handled in sales. Set tax settings defining whether the product is taxable and which tax rates apply. Upload product images to show customers what they're purchasing—add multiple images for different angles. Define product variants if selling the same item in different sizes, colors, or configurations—each variant can have distinct pricing. Enable inventory tracking if managing stock levels—set current quantity and reorder points. Create product bundles combining multiple items sold together at a package price—useful for promotions. Set up recurring billing for subscription products—define billing frequency and renewal terms. Import products in bulk using CSV upload for large catalogs—map file columns to product fields. Export the product list for analysis, backup, or sharing with other systems. Organize products using the Categories section in the left panel—create hierarchical category structures. Configure Units of Measure (UOM) for products sold by weight, volume, length, or other measurements. --- # Properties Published 2026-02-16 > How to use: Access properties management by selecting "Properties" from the WORKSPACE section in the settings sidebar. Choose which object type you want to customize by clicking on categories in the left panel: Customer, **How to use:** Access properties management by selecting "Properties" from the WORKSPACE section in the settings sidebar. Choose which object type you want to customize by clicking on categories in the left panel: Customer, Company, Product & Service, Team member, Inbox, Tickets, or Sales pipelines. Create a new property by clicking "+ Add field" within any property group—opens a form to define your custom field. Enter a clear property name that describes what information will be stored—use names that team members will understand immediately. Select the appropriate data type for your property: text, number, dropdown, checkbox, date, URL, email, phone, or multi-select. Add help text or descriptions that appear below the field to guide users on what information to enter. Configure dropdown or multi-select options by listing all possible choices users can select from. Set whether the property is required or optional—required fields must be filled before saving records. Create property groups to organize related fields together—group contact information separately from business details or preferences. Arrange properties in logical order by dragging them up or down within groups—put most important fields at the top. Control property visibility by setting which user roles can view or edit each field—protect sensitive information. Enable property history tracking to see when values changed and who changed them—useful for auditing and accountability. Set validation rules for data quality—minimum/maximum values for numbers, format requirements for phone or email fields. Create calculated properties that automatically compute based on other field values—useful for metrics or derived data. Make properties searchable so users can filter and find records based on custom field values in list views. Use conditional visibility to show or hide properties based on values in other fields—creates dynamic forms that adapt. Test new properties by creating sample records and verifying data captures, displays, and validates correctly before rolling out to the team. --- # Projects Published 2026-02-16 > How to use: View all operational projects by opening the Operation Projects dashboard—this shows active projects with their status, progress percentage, and project leads. Click on any project card to see full details in **How to use:** View all operational projects by opening the Operation Projects dashboard—this shows active projects with their status, progress percentage, and project leads. Click on any project card to see full details including objectives, timeline, budget, and team members. Create a new operational project by clicking "New Project" and filling in the project name, description, start and end dates, and budget if applicable. Add team members by clicking "Add Member" and selecting from your organization's staff list. Define project phases by creating milestones with specific dates and deliverables. Break down phases into individual tasks using the "Add Task" button within each phase. Set task dependencies by linking tasks that must be completed before others can start—this creates an automatic sequence. Track project progress by updating task completion status and marking milestones as done. Upload relevant documents to the project's file section for easy team access. Use the timeline or Gantt chart view to visualize the project schedule and identify potential delays. Communicate with the project team using the discussion or comments feature—tag specific members using @ mentions. Update project status (on track, at risk, delayed) so leadership understands current situation. Generate project reports showing time spent, budget used, and completion percentage. Archive completed projects to keep your active list manageable while preserving project history. --- # Frontline Inbox Published 2026-02-16 > How to use: Start your day by opening Frontline Inbox to see all new customer messages. The inbox shows conversations from all channels in one list—look for unread messages marked with a blue dot or bold text. Click on a **How to use:** Start your day by opening Frontline Inbox to see all new customer messages. The inbox shows conversations from all channels in one list—look for unread messages marked with a blue dot or bold text. Click on any conversation to open it and read the full message thread. To reply, type your response in the text box at the bottom and click "Send." If you need time to research an answer, click "Snooze" to temporarily remove the conversation from your active list—it will reappear at the time you set. Use the "Assign" button to transfer a conversation to a colleague who can better handle it. Add internal notes by clicking the note icon—these are only visible to your team, not the customer. Use filters at the top to view only messages from specific channels, unassigned conversations, or messages assigned to you. Mark conversations as "Resolved" when the customer's issue is completely handled. Check the conversation sidebar to see the customer's profile, previous interactions, and any related tickets or orders. --- # Tasks Published 2026-02-16 > How to use: Access your personal task list by clicking "My Tasks" in the main navigation. The default view shows all tasks assigned to you across the entire system. Sort tasks by due date, priority, or project using the **How to use:** Access your personal task list by clicking "My Tasks" in the main navigation. The default view shows all tasks assigned to you across the entire system. Sort tasks by due date, priority, or project using the sort dropdown menu. Click on any task to open it and see complete details, attachments, and comments. Update task status by clicking the status dropdown and selecting "Not Started," "In Progress," "Pending," or "Complete." Mark tasks complete by checking the checkbox next to the task name—completed tasks move to your "Done" section. Add time logs to track how long you spent on tasks by clicking "Log Time" and entering hours and minutes. Filter your tasks to see only high-priority items or tasks due this week using the filter options. Add personal notes or questions to tasks by typing in the comments section—these are visible to others assigned to the same task or project. Set reminders for important tasks by clicking the reminder icon and choosing when you want to be notified. If you're stuck or need input, mention colleagues in comments using @name. View tasks by project to see everything related to one project at once. Use the calendar view to see tasks distributed across dates, helping you plan your week. Create subtasks within larger tasks to break work into smaller, manageable steps. Delegate tasks to others by clicking "Reassign" if you're not the right person to handle it. --- # Frontline Reports Published 2026-02-16 > How to use: Access reports by clicking the "Reports" or "Analytics" tab in the navigation menu. Start with the dashboard overview that shows key metrics at a glance—response times, ticket volumes, and customer satisfacti **How to use:** Access reports by clicking the "Reports" or "Analytics" tab in the navigation menu. Start with the dashboard overview that shows key metrics at a glance—response times, ticket volumes, and customer satisfaction scores. Use the date range selector at the top to view data for today, this week, this month, or a custom period. Click on any metric or chart to drill down into more detailed information. Generate a team performance report by selecting "Team Reports" and choosing the time period—this shows how many tickets each person handled, their average response time, and customer ratings. Export any report to Excel or PDF using the "Export" button for sharing in meetings or keeping records. Set up automated reports by clicking "Schedule Report" and choosing email delivery frequency—daily, weekly, or monthly. Filter reports by channel (email, chat, phone) to see which communication method is busiest. Use the "Trends" view to compare current performance against previous periods and identify improvements or declines. Create custom reports by selecting "Custom Report Builder" and choosing which metrics to include. Save frequently used reports for quick access later. Share reports with managers by clicking "Share" and entering their email addresses. --- # Cycles Published 2026-02-16 > How to use: View all recurring cycles by opening the Cycles module—this shows currently active cycles, upcoming ones, and completion history. Create a new cycle by clicking "New Cycle" and naming it clearly (e.g., "Weekl **How to use:** View all recurring cycles by opening the Cycles module—this shows currently active cycles, upcoming ones, and completion history. Create a new cycle by clicking "New Cycle" and naming it clearly (e.g., "Weekly Inventory Check" or "Monthly Financial Reconciliation"). Define the cycle frequency by selecting daily, weekly, monthly, quarterly, or creating a custom schedule. Set which day(s) or date(s) the cycle should occur—for example, "Every Monday" or "First day of each month." Add all tasks that must be completed as part of this cycle using the "Add Task" button. Assign tasks to specific team members so everyone knows their responsibilities each cycle. Write clear instructions for each task so there's no confusion when the cycle generates. Set the cycle to automatically create the next instance when the current one is marked complete. Activate the cycle by clicking "Start Cycle"—the system will generate the first instance immediately or on the scheduled date. When a cycle instance appears on your task list, complete each task as you normally would. Mark the entire cycle complete once all tasks are done—this closes the current cycle and triggers the next scheduled instance. Edit cycle templates by clicking "Edit Cycle"—changes apply to future instances, not ones already in progress. Pause a cycle temporarily by clicking "Pause" if the routine is temporarily not needed. View cycle history to see completion rates, time taken, and identify patterns or delays. Use cycles for any routine work: daily reports, weekly team meetings, monthly maintenance, quarterly reviews, or annual audits. Delete cycles that are no longer relevant to your operations --- # Content CMS Published 2026-02-16 > How to use: Access your content dashboard showing all published pages, drafts, and scheduled content. Create a new page by clicking "New Page" and selecting page type (standard page, blog post, landing page, etc.). Write **How to use:** Access your content dashboard showing all published pages, drafts, and scheduled content. Create a new page by clicking "New Page" and selecting page type (standard page, blog post, landing page, etc.). Write your page title in the title field—this becomes the page headline and browser tab text. Use the visual editor to write and format content—highlight text to make it bold, italic, create headings, or add links. Insert images by clicking the image icon, uploading from your computer, or selecting from the media library. Resize and position images using drag handles and alignment options. Add buttons or call-to-action elements by clicking "Insert Element" and configuring button text, color, and destination URL. Create structured layouts using content blocks or sections—drag and drop blocks to rearrange page structure. Add videos by pasting YouTube or Vimeo URLs, or uploading video files directly. Optimize for search engines by filling in the SEO section with page description, keywords, and custom URL slug. Preview your page by clicking "Preview" to see exactly how it will appear to visitors on desktop and mobile devices. Save as draft if the content isn't ready to publish yet—drafts are only visible to content creators. Schedule publication by setting a future date and time instead of publishing immediately. Publish pages by clicking "Publish" when ready—content goes live instantly. Edit published content by opening the page and making changes—updates appear immediately after saving. Organize content using categories and tags for better site navigation and filtering. Duplicate existing pages as templates by clicking "Duplicate" rather than starting from scratch. Manage navigation menus by adding, removing, or rearranging links in the menu editor. Review analytics to see which pages get most traffic and engagement—use this to inform future content decisions. --- # Frontline Call Center Published 2026-02-16 > How to use: Log into the call center module at the start of your shift by clicking "Available" or "Ready"—this lets incoming calls reach you. When a call comes in, a pop-up appears showing the caller's number and name (i **How to use:** Log into the call center module at the start of your shift by clicking "Available" or "Ready"—this lets incoming calls reach you. When a call comes in, a pop-up appears showing the caller's number and name (if they're in the system). Click "Accept" to answer. During the call, the customer's profile and history automatically display on your screen—review this while speaking to provide personalized service. Take notes in the call notes field as you talk so you remember key details. If you need to transfer the call, click "Transfer" and select the appropriate team member or department from the list. To put a caller on hold, click the "Hold" button. After the call ends, the system prompts you to categorize the call and add any final notes—complete this before moving to the next call. Access call history by clicking "Call Logs" to see all past calls with timestamps, durations, and recordings. Listen to call recordings by clicking the play icon next to any call entry—useful for training or reviewing complex conversations. Use the dial pad to make outbound calls by entering the number and clicking "Call." Set your status to "Break" or "Away" when you need to step away so calls aren't routed to you. --- # Tourism TMS Published 2026-02-16 > How to use: View all scheduled tours in the tour calendar showing departures by date and tour type. Create a new tour by clicking "New Tour" and selecting the tour package or creating a custom itinerary. Set tour date, s **How to use:** View all scheduled tours in the tour calendar showing departures by date and tour type. Create a new tour by clicking "New Tour" and selecting the tour package or creating a custom itinerary. Set tour date, start time, and maximum capacity for the group. Assign a tour guide from your available staff by selecting their name from the guide dropdown. Add booking details when customers book a tour: customer name, number of people, special requests or dietary restrictions. Process payment or deposit and confirm the booking via email. Monitor tour capacity by checking the booking counter—you'll see spots remaining versus maximum capacity. Access the tour manifest before departure showing all booked participants with their details and special needs. Check in participants on tour day by marking them present in the system—this helps with headcount and no-show tracking. Coordinate transportation by confirming pickup times, vehicle assignments, and driver information in the system. Track supplier bookings (restaurants, activity providers, accommodations) linked to each tour—ensure all reservations are confirmed. Handle cancellations by finding the booking, clicking "Cancel," and processing refunds according to your cancellation policy. Modify bookings by opening the reservation and editing dates, participant numbers, or special requests. Generate tour costing reports to see expenses versus revenue for profitability analysis. Manage tour guide schedules to avoid double-booking guides and ensure adequate coverage. Create tour packages by defining itineraries with all included services, pricing, and descriptions. Update tour availability based on seasonal schedules or special events. Send pre-tour information to participants automatically including meeting location, what to bring, and itinerary highlights. --- # Automation Published 2026-02-16 > How to use: Create a new automation workflow by clicking "New Automation" or "Create Workflow" in the Automation module. Name your automation clearly describing what it does, like "Welcome New Customers" or "Follow-up In **How to use:** Create a new automation workflow by clicking "New Automation" or "Create Workflow" in the Automation module. Name your automation clearly describing what it does, like "Welcome New Customers" or "Follow-up Inactive Leads." Select a trigger that starts the automation: "Contact Created," "Tag Added," "Form Submitted," "Email Opened," "Purchase Made," or time-based triggers. Configure trigger conditions by adding filters—for example, trigger only when "Form = Contact Us Form" or "Product Category = Electronics." Add the first action that should happen when triggered by clicking "Add Action"—choose from sending emails, adding tags, creating tasks, sending notifications, or updating contact fields. Set delays between actions by adding a "Wait" step—wait 1 day, 3 days, 1 week, or until a specific time before the next action. Branch your workflow using conditional logic: "If contact opened email → Send follow-up; If not opened → Send reminder"—this creates different paths. Send automated emails by adding "Send Email" action and selecting an email template or creating new content—personalize with contact field variables. Add or remove tags automatically to track contact status and behavior throughout the automation journey. Create tasks for team members by adding "Create Task" action and assigning to the appropriate person—useful for requiring human touchpoints in automated workflows. Update contact properties automatically, such as changing "Status" field to "Active Customer" or adding lifecycle stage updates. Add contacts to other automations or segments as an action, creating cascading workflows for complex customer journeys. Set enrollment limits so contacts only go through the automation once, or allow re-enrollment for recurring workflows. Test your automation before activating by using the test mode—walk through the workflow with a sample contact to verify it works correctly. Activate the automation by toggling it "On"—from this point, contacts meeting trigger criteria automatically enter the workflow. Monitor automation performance through the analytics dashboard showing enrollment numbers, completion rates, and action success rates. Pause automations temporarily without deleting them by toggling to "Paused" status—useful during updates or when processes change. Edit active automations carefully as changes affect contacts currently in the workflow—consider duplicating and creating a new version instead. View individual contact progress through workflows by checking their profile timeline showing which automation steps they've completed. --- # Contacts Published 2026-02-16 > How to use: Access your contacts database by clicking "Contacts" in the main navigation—this displays all people and companies in your system. View contact information by clicking on any contact name to see their full pr **How to use:** Access your contacts database by clicking "Contacts" in the main navigation—this displays all people and companies in your system. View contact information by clicking on any contact name to see their full profile including personal details, communication history, purchase records, and related tickets or projects. Add a new contact manually by clicking "New Contact" or "Add Contact" and filling in required fields like name, email, phone number, and company. Import multiple contacts at once by clicking "Import" and uploading a CSV or Excel file with contact data—the system maps your columns to contact fields. Search for specific contacts using the search bar at the top—type names, email addresses, phone numbers, or company names. Filter contacts using advanced filters to find groups like "customers in New York" or "contacts added this month"—combine multiple criteria for precise results. Edit contact information by opening the contact profile and clicking "Edit"—update any outdated details like addresses, phone numbers, or job titles. Add custom fields to store unique information relevant to your business by going to contact settings and creating new field types. Tag contacts with relevant labels (VIP, Newsletter Subscriber, Event Attendee) by selecting contacts and clicking "Add Tag"—tags help organize and filter. Merge duplicate contacts by selecting the duplicates and clicking "Merge"—choose which information to keep from each record. Track all interactions automatically as the system logs emails, calls, tickets, and purchases on each contact's timeline. Add manual notes to contact profiles by clicking "Add Note" and typing important information from conversations or meetings. Assign contacts to team members as account owners by selecting the owner from the dropdown—this clarifies who manages which relationships. Export contact lists by selecting contacts and clicking "Export"—download as CSV for use in other tools or backup purposes. Delete contacts by selecting them and clicking "Delete," but note this also removes their history—consider marking inactive instead. View contact segments they belong to in the profile sidebar showing which automated groups include this person. --- # POS Published 2026-02-16 > How to use: Open the POS interface at your sales station or device. Start a new sale by clicking "New Sale" or scanning the first item's barcode. Add products manually by searching for the item name or clicking product c **How to use:** Open the POS interface at your sales station or device. Start a new sale by clicking "New Sale" or scanning the first item's barcode. Add products manually by searching for the item name or clicking product categories and selecting items. Adjust quantities using the plus and minus buttons or by typing the number. Apply discounts by clicking "Discount" and entering percentage or dollar amount—you may need manager approval for large discounts. Add customer information to the transaction by searching for existing customers or quickly adding a new customer profile. Review the items in the cart on the right side of the screen before completing the sale. Click "Checkout" or "Pay" when ready to process payment. Select payment method: cash, credit card, debit card, or mobile payment. For card payments, prompt the customer to insert, tap, or swipe their card and follow the payment terminal's prompts. For cash payments, enter the amount received and the system calculates change due. Print or email the receipt by choosing the customer's preference. Process returns by clicking "Return" or "Refund," scanning items being returned, and selecting the original transaction if needed. Issue store credit or refund to original payment method based on your store's policy. Handle split payments by clicking "Split" and processing partial amounts on different payment methods. Place transactions on hold if a customer needs to step away temporarily by clicking "Park Sale." Retrieve parked sales from the holds list when the customer returns. Close out your register at end of shift by clicking "End Shift" and following the cash count procedure. Review daily sales reports to see total revenue, items sold, and payment method breakdown. --- # Frontline Tickets Published 2026-02-16 > How to use: Create a new ticket by clicking the "+" or "New Ticket" button. Fill in the ticket form with the customer's name, issue description, and priority level (low, medium, high, urgent). Select the appropriate cate **How to use:** Create a new ticket by clicking the "+" or "New Ticket" button. Fill in the ticket form with the customer's name, issue description, and priority level (low, medium, high, urgent). Select the appropriate category from the dropdown menu—this helps route the ticket correctly. Assign the ticket to yourself or a team member using the "Assignee" field. Set a due date to ensure timely resolution. Once created, the ticket appears in the tickets list. Click on any ticket to view full details and add updates. As you work on the issue, add comments describing what actions you've taken—this creates a clear history. Change the ticket status as you progress: from "Open" to "In Progress" when you start working, then to "Pending" if waiting on the customer or another department, and finally to "Resolved" when complete. Attach relevant files using the paperclip icon. Use tags to categorize tickets for easier searching later. View all your assigned tickets by selecting "My Tickets" from the filter menu. Managers can use the dashboard view to see all open tickets, who's handling what, and which tickets are overdue. --- # Tourism PMS Published 2026-02-16 > How to use: Open the property management dashboard to see today's arrivals, departures, and current occupancy at a glance. Create a new reservation by clicking "New Booking" and entering guest name, contact information, **How to use:** Open the property management dashboard to see today's arrivals, departures, and current occupancy at a glance. Create a new reservation by clicking "New Booking" and entering guest name, contact information, arrival and departure dates. Select room type from available options—the system shows only rooms available for those dates. Confirm the rate and any special packages or add-ons selected by the guest. Collect payment or deposit according to your property's policy using the integrated payment processor. Send booking confirmation to the guest automatically via email with all reservation details. Update the room assignment as check-in approaches by clicking on the reservation and selecting the specific room number. Check guests in on arrival day by finding their reservation and clicking "Check In"—this changes room status and triggers housekeeping notification. Issue physical or digital room keys using your key card system integration. Handle room changes by finding the reservation, clicking "Move Room," and selecting the new room—system updates all records. Process additional charges by adding items to the guest folio—room service, minibar, spa services, etc. View the housekeeping board to see which rooms are clean, dirty, or under maintenance—update statuses as cleaning is completed. Mark rooms out of order if maintenance is needed by clicking the room and selecting "Out of Order" with a reason. Check guests out by calculating final bill, reviewing charges, processing payment, and updating room status to "Dirty." Manage room rates by accessing the rate calendar and setting prices for different dates, seasons, or events. Generate occupancy reports to see booking patterns and optimize pricing strategy. Block rooms for maintenance or personal use so they don't appear in availability. --- # Frontline Knowledge Base Published 2026-02-16 > How to use: Search the knowledge base by typing keywords into the search bar at the top—the system shows relevant articles as you type. Browse articles by category by clicking on category names in the sidebar. Open any a **How to use:** Search the knowledge base by typing keywords into the search bar at the top—the system shows relevant articles as you type. Browse articles by category by clicking on category names in the sidebar. Open any article to read the full content, see related articles, and view helpful ratings from other users. If you can't find an answer, request a new article by clicking "Request Article" and describing what information is needed. Create new articles (if you have permission) by clicking "New Article" and writing the content in the editor—use headings, bullet points, and images to make articles easy to scan. Organize articles into appropriate categories and add relevant tags for better searchability. Use the formatting toolbar to bold important points, add numbered steps, or insert screenshots. Before publishing, click "Preview" to see how the article will appear. Save articles as drafts if they're not ready to publish yet. After publishing, track article performance by viewing how many times it's been read and its helpfulness rating. Update outdated articles by clicking "Edit" on any article you created. Add related articles links at the bottom to guide readers to additional relevant information. For external-facing knowledge bases, click "Publish Externally" to make articles visible to customers on your website. Use version history to restore previous versions if needed. --- # Triage Published 2026-02-16 > How to use: Open the Triage queue to see all new, unprocessed items that need classification. Review each item starting from the top of the queue. Read the item's basic information—subject, source, customer name, and ini **How to use:** Open the Triage queue to see all new, unprocessed items that need classification. Review each item starting from the top of the queue. Read the item's basic information—subject, source, customer name, and initial description. Determine the urgency level by assessing whether it requires immediate attention, can wait a few hours, or is routine. Click on the priority dropdown and select "Urgent," "High," "Normal," or "Low." Categorize the item by selecting the appropriate type from the category menu—such as "Sales Inquiry," "Technical Issue," "Billing Question," or "General Information." Assign the item to the right person or team by clicking "Assign To" and choosing from the list based on expertise and current workload. Add initial notes or context that will help the assignee understand the situation quickly. If an item is spam or irrelevant, click "Dismiss" or "Mark as Spam" to remove it from the queue. Convert items into the appropriate format—click "Create Ticket" for issues requiring follow-up, "Add to Sales Pipeline" for sales opportunities, or "Forward to Team" for general inquiries. Set due dates for time-sensitive items so assignees know the expected turnaround time. Use quick filters to show only items from specific sources or types if the queue is overwhelming. Work through the queue systematically to ensure nothing sits unprocessed for too long. Check the triage dashboard to see how many items are processed per hour and identify bottlenecks. --- # Segments Published 2026-02-16 > How to use: Create a new segment by clicking "New Segment" or "Create Segment" in the Segments module. Name your segment clearly to describe the group, such as "Active Customers Last 30 Days" or "Leads from Website Form. **How to use:** Create a new segment by clicking "New Segment" or "Create Segment" in the Segments module. Name your segment clearly to describe the group, such as "Active Customers Last 30 Days" or "Leads from Website Form." Choose between dynamic (updates automatically) or static (fixed list at creation time) segment types—dynamic segments are usually more useful. Define segment criteria by adding conditions using the condition builder—select fields like "Email Status," "Last Purchase Date," or "Contact Source." Combine multiple conditions using AND/OR logic: "Location is San Francisco AND Last Purchase within 30 days" creates a precise segment. Use date-based conditions for time-sensitive segments like "Birthday this month" or "Account created over 1 year ago"—these update automatically as dates change. Filter by behavioral data such as "Opened last 3 emails" or "Clicked link in campaign" to find engaged contacts. Include or exclude contacts based on tags by adding tag conditions—"Has tag: VIP" or "Does not have tag: Unsubscribed." Set numerical conditions for values like "Total purchases greater than $500" or "Number of support tickets equals 0." Preview segment results before saving by clicking "Preview"—see how many contacts match and review a sample list. Save the segment to make it available throughout the system for targeted campaigns, reports, and automation. Use segments for targeted email broadcasts by selecting the segment as your recipient list when creating campaigns. Apply segments in reports to analyze specific customer groups—compare behavior across different segments. Update segment criteria anytime by editing the segment—dynamic segments automatically recalculate membership. View segment growth over time using the segment analytics dashboard showing how membership numbers change. Export segment members by clicking "Export Contacts" to get a list of everyone currently in that segment. Nest segments by creating conditions like "Is in segment: Active Customers" combined with other criteria for complex targeting. Delete segments that are no longer useful, but note that automations or campaigns using that segment may need updating. --- # Frontline Forms Published 2026-02-16 > How to use: Create a new form by clicking "Create Form" and choosing between starting from scratch or using a template. Give your form a clear title that explains its purpose. Add fields by clicking "Add Field" and selec **How to use:** Create a new form by clicking "Create Form" and choosing between starting from scratch or using a template. Give your form a clear title that explains its purpose. Add fields by clicking "Add Field" and selecting the type—text input, multiple choice, checkbox, dropdown, date picker, or file upload. For each field, write a clear label and mark it as required or optional. Arrange fields in logical order by dragging and dropping them. Use the "Logic" feature to show or hide certain fields based on previous answers—for example, only show shipping address fields if someone selects "Physical product." Preview your form by clicking "Preview" to see how it looks to users. Customize the form's appearance using the design tab to match your branding. Once ready, click "Publish" to make the form active. Copy the form link to share via email or messaging, or use the embed code to add it to your website. View form responses by clicking "Responses" or "Submissions"—each response is saved with timestamp and respondent information if provided. Export responses to a spreadsheet using the "Export" button. Convert form submissions into tickets automatically by enabling the "Create Ticket" option in form settings. Edit forms after publishing by clicking "Edit"—existing responses remain saved. Disable forms when no longer needed by clicking "Deactivate." --- # Sales Pipeline Published 2026-02-16 > How to use: View your pipeline by opening the Sales Pipeline module—leads and opportunities are displayed in columns representing each stage of your sales process. Add a new lead by clicking "Add Lead" or "New Opportunit **How to use:** View your pipeline by opening the Sales Pipeline module—leads and opportunities are displayed in columns representing each stage of your sales process. Add a new lead by clicking "Add Lead" or "New Opportunity" and entering the prospect's name, company, contact information, and estimated deal value. Drag and drop opportunities between pipeline stages as they progress—from "Lead" to "Qualified" to "Proposal Sent" to "Negotiation" to "Closed Won" or "Closed Lost." Click on any opportunity card to open detailed information and see full history. Log all interactions with the prospect by adding notes after each call, email, or meeting—include date, summary, and next steps. Set expected close date so you and your manager can forecast revenue accurately. Update deal value if the scope or price changes during negotiations. Assign probability percentages to opportunities based on likelihood of closing—this helps with realistic forecasting. Schedule follow-up tasks directly from the opportunity by clicking "Add Task" and setting reminders. Attach proposals, contracts, and other documents to the opportunity record for easy reference. Filter the pipeline to see only your deals, deals above a certain value, or deals closing this month. Use the pipeline report view to see total value at each stage and projected revenue. Move lost deals to "Closed Lost" and add a reason (price, timing, chose competitor) to help analyze why deals don't close. Celebrate wins by marking deals "Closed Won"—these opportunities move to completed deals and count toward your targets. Review aging opportunities that haven't moved stages in a while and take action to advance or disqualify them. Set reminders for important dates like proposal deadlines or contract expiration dates. --- # 1. Getting Started with the Operation Plugin Published 2025-11-04 > 1. Getting Started with the Operation Plugin The Operation plugin is one of the core modules in erxes, designed to help your team manage tasks, organize projects, assign responsibilities, and track progress effectively. ## 1\. Getting Started with the Operation Plugin The Operation plugin is one of the core modules in erxes, designed to help your team manage tasks, organize projects, assign responsibilities, and track progress effectively. If you're new to this plugin, follow the steps below to get started quickly and confidently. ### **What is the Operation Plugin?** The Operation plugin allows you to manage your work using **Tasks**, **Projects**, and **Cycles**. It’s ideal for tracking both individual assignments and larger, team-based goals. 🧭 **Navigation**: Find the Operation plugin in the left sidebar of your erxes workspace. ### **Step 1: Set Up Your Team** Every task in Operation must be associated with a **Team**. Before you create a task, make sure you either join an existing team or create one. 🛠 If you have admin access: - Go to **Settings → Teams → + Create Team** to create a new team. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ef9953be-b804-4aae-6d65-ced20a88b200/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/ecd81a94-0ef2-4179-ed2f-738b14478d00/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/9d44b5af-1418-4b8c-11f0-e2f581165c00/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/68b9ba1b-bfd0-4391-fdb4-8c3c5c63d900/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/175de4fe-420b-4429-8ab3-b1daab027200/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1230f927-2f4b-4688-ca8a-d6f9514dc500/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cd01e3d6-e8be-486d-2e53-fabb391f7000/public) ### **Step 2: Create Your First Task** Tasks represent actionable items. Creating a task helps track progress, assign ownership, and clarify priorities. 📝 To create a task: 1. Go to the Operation plugin. 2. Click the **"Create Task"** button. 3. Add a **title** for your task. 4. Select the **Team** the task belongs to. 5. Optionally, fill in additional fields like **assignee**, **due date**, **priority**, or **labels**. 6. Click **Save** to create the task. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cb4b6732-5c64-4e7c-9049-e1fab2b8a500/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/314839f9-3e07-4c5a-5c1f-5503e6a4f300/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/4ced744e-103f-4a5e-2f05-b85161979700/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/501e5ff7-2059-400a-3dff-21bb16526700/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1ca588f2-d168-4254-aa40-bb8cff8fd500/public) 💡 Note: A **Team selection is required** for every task. ### **Step 3: Locate Your Task** Once a task is created, you can view it in the following places: - **My Tasks → Created by me** - **Team  → Task tab** ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8e6e2d69-f0bd-465a-3e41-fab4a6cd2900/public) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6aeb3423-93be-480a-e261-fe0effd74800/public) 👁 If the task is assigned to you, it will also appear under **My Tasks → Assigned to me**. --- # 6. Navigating Your Tasks: Views, Filters Published 2025-11-04 > The Operation plugin allows you to effectively manage and monitor your tasks using views and filters . This helps you focus on what matters most and quickly access relevant tasks. 📄 Task Views You can view your tasks in The Operation plugin allows you to effectively manage and monitor your tasks using **views** and **filters**. This helps you focus on what matters most and quickly access relevant tasks. ### 📄 **Task Views** You can view your tasks in different visual formats depending on your workflow: - **List View** – Shows tasks in a simple vertical list - **Board View** – Displays tasks as columns based on their statuses (Kanban-style) > You can switch between views from the **“Views”** dropdown in the top-right corner. ### 🔍 **Filters** Filters help you narrow down your task list to only what's relevant to you. You can filter tasks by: - **Status** - **Assignee** - **Team** - **Project** - **Cycle** - **Tag** - **Priority** (Critical, High, Medium, Low, No Priority) > You can combine multiple filters and save them as a **Custom View** for quick access later. **→ Next: Automating Task Workflows and Assignments** --- # 5. How to Track and Prioritize Your Work Published 2025-11-04 > The Operation plugin helps you monitor your tasks and keep focus on the most important ones. By using Status , Priority , Tag , and View configuration , you can better manage your workload and make smarter decisions abou The Operation plugin helps you monitor your tasks and keep focus on the most important ones. By using **Status**, **Priority**, **Tag**, and **View configuration**, you can better manage your workload and make smarter decisions about what to tackle next. ### 📌 **Task Status** Each task has a **status** that reflects its current progress. Statuses are customizable per team, but commonly include: - Backlog: Planned but not yet started - Todo: Ready to begin - In progress: Currently being worked on - Blocked: Stuck or awaiting input - Done: Completed - Cancelled: No longer needed > All status changes are logged in the task’s activity feed for team visibility. ### ⭐️ **Priority Levels** You can assign one of five **priority levels** to a task: - **Critical** – Extremely urgent and important - **High** – Important and time-sensitive - **Medium** – Moderate importance - **Low** – Can be delayed if needed - **No Priority** – No urgency or priority assigned Priority is a key factor in determining how tasks are sorted and displayed across different views. ### 🏷 **Using Tags** Tags are useful for categorizing and organizing tasks by **topic**, **department**, or **context**. You can: - Create tags at the team level - Add multiple tags to a task - Filter and group tasks by tag > This helps you quickly locate related tasks or identify patterns across your projects. ### **Task Views** Choose from different views to manage your work more effectively: - **List view** – A simple, vertical task list - **Board view (Kanban)** – Columns based on status for workflow management ### ✅ **Tips for Prioritizing Effectively** - When creating a task, always set the **Priority**, **Due date**, and optionally add to a **Cycle** - Tackle **Critical** and **High** priority tasks first - Identify and unblock tasks marked as Blocked - Move completed work to Done to reduce clutter ### Next step: **→ Navigating Your Tasks: Views, Filters** --- # 4. Organize Your Work Using Teams and Cycles Published 2025-11-04 > The Operation plugin helps you organize your work effectively by structuring tasks around Teams and Cycles . By assigning tasks to teams and distributing them across time-bound cycles, you can improve team collaboration, The Operation plugin helps you organize your work effectively by structuring tasks around **Teams** and **Cycles**. By assigning tasks to teams and distributing them across time-bound cycles, you can improve team collaboration, planning accuracy, and performance tracking. ### 👥 **What is a Team?** A **Team** represents a group of users working together within your organization or on a shared product area. Each team has its own: - **Tasks (Issues)** - **Projects** - **Cycles** - **Custom workflows and views** ### **How to Create or Join a Team** **To create a new team:** 1. Go to **Sidebar → Settings → Teams** 2. Click **"Add Team"** 3. Fill in the team name, description, permissions, and other configurations **To join an existing team:** - Hover over the team name in the sidebar and click **Join team** ### **What is a Cycle?** A **Cycle** is a time-bound period (e.g., 2 weeks) during which a team plans, works on, and completes tasks. It functions similarly to an Agile sprint. ### **How to Create a Cycle** 1. Navigate to the **Cycles** section from the sidebar 2. Click **"Create Cycle"** 3. Define the cycle name, start and end dates, and select the team it belongs to 4. Save to create the new cycle under that team ### **Assigning Tasks to a Cycle** - From the task detail view, use the **Cycle** field to select a cycle - If the task’s due date falls within an active cycle, it will automatically be assigned to that cycle ### 🎯 **Why Use Teams and Cycles?** - Each team can independently manage its own workflow and focus areas - Cycles help teams plan short-term work and evaluate progress in a measurable way - Improves visibility, accountability, and team velocity over time **→Next step: How to Track and Prioritize Your Work** --- # 3. Create Your First Task and Project Published 2025-11-04 > The Operation plugin is designed to help users and teams manage their daily work in an organized and trackable way. The first step to getting started is to create your first Task and Project . 📝 What is a Task? A task i The **Operation plugin** is designed to help users and teams manage their daily work in an organized and trackable way. The first step to getting started is to create your first **Task** and **Project**. ### 📝 What is a Task? A task is a specific piece of work to be completed. It contains the following: - Must belong to a **Team** - Has a **Title**, **Description**, **Task ID**, **Status**, and **Assignee** - Can include additional properties like **Cycle**, **Project**, **Due date**, **Labels**, etc. ### How to Create a Task 1. Click the **“Create Task”** button (found at the top or bottom right corner) 2. Enter a **Task Title** (e.g., “Prepare social media post”) 3. Write a detailed **Description** for the task 4. Select a **Team** (required) 5. Optionally, you can also set: - **Assignee**, **Due Date**, **Labels**, **Project**, **Cycle** 6. Click **Save** to create your task ### 📂 What is a Project? A Project is a way to group and manage multiple tasks under one goal. Examples include launching a new website, running a marketing campaign, or releasing a product update. ### How to Create a Project 1. Go to **Sidebar > Projects** 2. Click the **“Create Project”** button 3. Enter a **Project Name** and **Description** 4. Select a **Team** 5. Optionally configure: - **Milestone**, **Assignee**, **Status**, **Timeline**, **Labels**, etc. 6. Click **Save** to create the project ### 🎯 Benefits - Creating tasks helps clarify ownership and responsibility - Projects help organize multiple tasks toward a shared objective - Later, you can manage tasks and projects using **Cycles**, **Teams**, and **Views** ### 👉 Next Step: **Organize Your Work Using Teams and Cycles** --- # 2. Understanding the Operation Workspace Structure Published 2025-11-04 > The Operation plugin is designed to help you and your team manage work efficiently. The workspace is made up of key components: Tasks , Projects , Cycles , and Teams . Understanding how these components work together is The **Operation plugin** is designed to help you and your team manage work efficiently. The workspace is made up of key components: **Tasks**, **Projects**, **Cycles**, and **Teams**. Understanding how these components work together is essential for staying organized and productive. ### **1\. Tasks** Tasks are the smallest unit of work within the system. - Every task **must belong to a team** - Tasks include details like: **title**, **status**, **task ID**, **assignee**, **due date**, **labels**, **project**, **cycle**, and more - Tasks support **different views**, **filters**, and **keyboard shortcuts** ### **2\. Projects** Projects help you **group related tasks under a common goal or theme**. - Projects are often used for **large initiatives**, **campaigns**, or **internal processes** - They allow you to manage tasks in **a more structured and visual way** - Projects can be organized using **milestones** to track major phases or deliverables ### **3\. Cycles** Cycles are **time-based work sprints** that provide structure to your planning. - Typically used for **bi-weekly, monthly, or custom timeframes** - When a task is added to a cycle, it’s scheduled within that time period - Helps improve **team focus, planning, and review** - Each cycle includes a **start and end date**, **target tasks**, and **completed outcomes** ### **4\. Teams** Teams represent a group of users managing a shared set of work. - All tasks, projects, and cycles are organized under teams - Users can be part of **multiple teams** - Each team has its own **workflow**, **statuses**, **labels**, and **integrations** - Teams can be **public or private** ### **5\. Views & Filters** - Different sections like **Issues**, **My Tasks**, **Team Issues**, **Projects**, and **Cycles** have their own views - Each view supports filters like **status**, **assignee**, **priority**, **labels**, and more - You can create **custom views** for quick access later ### **Key Considerations:** - It's recommended to **set up your team and cycles** before creating tasks - Tasks are structured as: **Task → Project → Cycle → Team** - These layers help you **visualize, manage, and scale** your work more effectively ### **Next Step:** 👉 **Create Your First Task and Project** --- # 4. How to Connect erxes Messenger to Your Website Published 2025-10-15 > Description: The erxes Messenger integration allows you to add a live chat widget to your website. Once connected, visitors can chat with your team directly through your site, and all messages will appear in Frontline In **Description:** The **erxes Messenger** integration allows you to add a live chat widget to your website. Once connected, visitors can chat with your team directly through your site, and all messages will appear in **Frontline Inbox** under the selected **Channel**. ### **I. Quick Setup** 1. Go to your **Channel**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a5dfcf1f-5356-4e64-c63c-84c444852300/public) 2. Click **erxes Messenger**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/475c772a-a57e-44e7-00cf-ee6547747500/public) 3. Click **\+ Add Messenger**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/fad5d118-4e77-405e-a825-c0ac46d8bc00/public) 4. In the **Name** field, enter your website link (for example: _erxes.io_) ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/8b96d6b9-29a3-462a-4e62-c2bc35bdda00/public) 5. Click **Save** to finish setup   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b7c2b15b-2364-47e2-49f9-8a8f418dce00/public) ### **II. Detailed Setup** 1. Go to your **Channel** 2. Click **erxes Messenger** 3. Click **\+ Add Messenger** #### **Step 1 – Appearance** - **Brand Color** – Choose your widget’s main color - **Text Color** – Choose the text color - **Logo** – Upload your brand logo #### **Step 2 – Greeting** - **Greeting Title** – Add a short welcome title - **Greeting Message** – Write a friendly welcome message - **Supporter** – Assign the team member or bot who will respond - **Social Links** – Add links to your social media accounts #### **Step 3 – Messages** - **Online Message** – Message shown when a user sends a chat during working hours - **Offline Message** – Message shown when a user sends a chat outside working hours #### **Step 4 – Availability** - **Turn Online/Offline Manually** – Switch your messenger status manually - **Follow Your Schedule** – Automatically set online/offline hours based on your schedule - **Response Rate** – Set your estimated reply time - **Timezone** – Choose your timezone - **Display Operator Timezone** – Show the operator’s timezone in chat - **Hide Messenger During Offline Hours** – Hide the widget when your team is offline #### **Step 5 – Behavior & Access** - **Default Language** – Select your preferred language - **Require Authentication** – Ask visitors for email and phone before chatting - **Show Chat** – Hide the chat section and show only knowledge base or forms - **Show Launcher** – Keep the widget invisible but still collect data - **Force Logout** – Automatically end a user session when the conversation is resolved - **Notify Customer** – Send an email if the user is offline and the operator responds - **Show Video Call Request** – Allow operators to send video call requests #### **Step 6 – Integration Setup** - **Integration Name** – Give your Messenger integration a name - **Bot Setup** – Use this if you want to create automated responses - **Cloudflare Calls Setup** – Coming soon: allows direct voice calls from Messenger **💡Tips** - You can always edit Messenger settings later from the **Integration Settings** page. - Customize your greeting and colors to match your website’s branding for a better user experience. --- # 3. How to Connect Facebook Post Published 2025-10-15 > Description: Connecting your Facebook Post integration allows you to receive and manage comments from your Facebook Page posts directly inside Frontline . Each Facebook Post integration should be linked to a specific Cha **Description:** Connecting your **Facebook Post** integration allows you to receive and manage comments from your Facebook Page posts directly inside **Frontline**. Each Facebook Post integration should be linked to a specific **Channel**. ### **Steps to Connect Facebook Post** 1. Go to your **Channel**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/dc60db78-c4ae-4a29-c853-c147077a3500/public) 2. Select **Facebook Post** from the list of integrations   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/16b785f8-d830-4f7e-ae05-1a50cd1fbc00/public) 3. Click **\+ Add Facebook Post Integration**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/6aa309d8-976f-49f9-2a55-834764a5ab00/public) 4. Click **Connect Facebook Account**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/218ad23d-c64c-41ea-cba3-b5c1115fea00/public) 5. Select your **Facebook Profile Name**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/bfbd8483-bc80-4fa7-271b-60e654675b00/public) 6. Click **Next**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/1ea309a5-3e89-4f19-1336-bf0b1932ae00/public) 7. Choose the **Facebook Page** you want to connect   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/2c9c4a7c-1291-4507-165b-df414bd1b000/public) 8. Click **Next** again   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/b490c7c2-8583-4535-1920-ccdfc856d400/public) 9. Enter a name for your integration (for example, _Marketing Facebook Post_)   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/622a2098-7274-40d8-a95d-c91c88f8a700/public) 10. Click **Save** to complete the setup   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/e4b01a94-6863-4381-5e9e-b5f8be60a900/public) ### **💡Tips** - Make sure your **Facebook account** has admin access to the Page you want to connect. - If your business manages multiple Facebook Pages, connect each Page’s Post integration to a separate Channel for better organization and reporting. --- # 1. How to Create a Channel Published 2025-10-15 > Description What is a Channel? A Channel helps you organize different customer communication sources (like Facebook Messenger, Page Posts, or Website Chat) into separate groups. Each Channel can only be connected to one ### **Description** **What is a Channel?** A **Channel** helps you organize different customer communication sources (like Facebook Messenger, Page Posts, or Website Chat) into separate groups. Each Channel can only be connected to **one integration**. ### **Why Use Channels?** Channels allow you to manage communication by product, team, or service type. This keeps each team’s conversations, reports, and customers organized and easy to track. ### **Steps to Create a Channel** 1. Go to **Frontline**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/61101670-f3fe-45b5-5bf2-f919c3398700/public) 2. Click the **three dots (⋮)** next to the **Inbox**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/58a57038-8254-41ff-82bb-abaaa3131600/public) 3. Select **Settings**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/91be92af-06d4-4388-a0d9-8e3d26c53d00/public) 4. Under the **Frontline** category, click **Channel**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/a26e2e5d-8c25-48cd-ea46-b1565288b500/public) 5. Click **Add Channel**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/0685a645-2e2d-44c5-6cb3-9bbb31aef600/public) - Enter the **Channel Name**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/51e7a02f-fd66-4dfd-505a-bfaacfc36600/public) - Select **Members** who will be part of this Channel   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/f0d2beed-5457-42a3-66d5-52ea3fb54c00/public) - Click **Create**   ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/cab22dfe-2eca-4311-edce-d744c872c500/public) 6. You’ll now see your new Channel listed in the **Channel** section ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/7d7514bd-1850-49d1-b0bf-6c49db914500/public) ### 💡Tips - Create Channels based on **teams, products, or customer types** to keep your workspace organized. - If your company manages multiple **Facebook Pages**, it’s best to connect each one to a **separate Channel** for clarity and reporting accuracy. --- # 5. Using the Team inbox Module Published 2025-10-15 > Description: The Teaminbox module brings together all customer conversations in one place, enabling Team members to reply, filter, label, assign, and resolve conversations efficiently. Accessing Teaminbox Go to the Teami **Description:** The **Teaminbox** module brings together all customer conversations in one place, enabling **Team members** to reply, filter, label, assign, and resolve conversations efficiently. ### **Accessing Teaminbox** 1. Go to the **Teaminbox** module from the sidebar. 2. You will see a list of all current and past conversations. ### **Reply to a Conversation** 1. Click on a conversation to open it. 2. In the **"To send your message"** field, type your response. 3. Click **Send** to reply. ### **Resolve a Conversation** 1. Open a conversation. 2. Click the **Resolve** button in the top right corner to mark the conversation as closed. Note: If the **Customer** replies again, the conversation will reopen automatically and return to active status. 1. To view resolved conversations: Go to **Status** → **Resolved**. ### **Assign a Conversation to a Team Member** 1. Open a conversation. 2. In the **"Assign to: Member"** dropdown (top left), choose the **Team member** who will handle the response. 3. This assigns responsibility for the conversation. ### **Filter Conversations** 1. Click the **three lines (☰)** icon in the top-left corner. 2. Filter by: - **Channel** (e.g., Webchat, Email, Messenger) - **Brand** - **Segment** - **Integration** - **Tag** 1. Click the desired filter value to display matching conversations. Example: Selecting **Integration → Facebook Messenger** will show only Facebook conversations. ### **Filter Conversations by Date** 1. Click the **Date** filter in the Teaminbox view. 2. Choose the date range and click **Filter**. 3. All relevant conversations will appear. --- # 5. Using the Teaminbox Module Published 2025-10-15 > The Teaminbox module brings together all customer conversations in one place, enabling Team members to reply, filter, label, assign, and resolve conversations efficiently. Accessing Teaminbox Go to the Teaminbox module f The **Teaminbox** module brings together all customer conversations in one place, enabling **Team members** to reply, filter, label, assign, and resolve conversations efficiently. ### **Accessing Teaminbox** 1. Go to the **Teaminbox** module from the sidebar. 2. You will see a list of all current and past conversations. ### **Reply to a Conversation** 1. Click on a conversation to open it. 2. In the **"To send your message"** field, type your response. 3. Click **Send** to reply. ### **Resolve a Conversation** 1. Open a conversation. 2. Click the **Resolve** button in the top right corner to mark the conversation as closed. Note: If the **Customer** replies again, the conversation will reopen automatically and return to active status. 1. To view resolved conversations: Go to **Status** → **Resolved**. ### **Assign a Conversation to a Team Member** 1. Open a conversation. 2. In the **"Assign to: Member"** dropdown (top left), choose the **Team member** who will handle the response. 3. This assigns responsibility for the conversation. ### **Filter Conversations** 1. Click the **three lines (☰)** icon in the top-left corner. 2. Filter by: - **Channel** (e.g., Webchat, Email, Messenger) - **Brand** - **Segment** - **Integration** - **Tag** 1. Click the desired filter value to display matching conversations. Example: Selecting **Integration → Facebook Messenger** will show only Facebook conversations. ### **Filter Conversations by Date** 1. Click the **Date** filter in the Teaminbox view. 2. Choose the date range and click **Filter**. 3. All relevant conversations will appear. ### **Use Canned Responses** 1. In the reply area, type a keyword from a saved/canned response. 2. Choose from the suggested responses. 3. Click **Send** to send the selected message. ### **View a Customer’s Conversation History** 1. On the right-hand side, under the **Customer Info** panel, click **Activity**. 2. You will see all previous messages and calls with that **Customer**. ### **View Customer Feedback (Tickets)** 1. In the **Customer Info** panel, click **Related**. 2. Click on **Ticket** to view related feedback or support requests.   **Illustrated instructions** **1\. Go to the Teaminbox module** - You will enter **the Teaminbox module.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FwY5vutPEim21Fvfg827WVchoseinbox.png) - You will see a list of all conversations. **Reply to conversation** 1. Click on the unit conversation from the user. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FQSJvBhe6ZRkg82aZzs6rZchoseconversation.png) 2. In the " **To send your message** " section, users can write the content they want to reply to, ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7k2pvsIdDyMLmBqNqy316contenntt.png) 3. Click the **Send button to send.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fi3zDzMn4tUBZlYW20Yvy6sendd.png) **Conversation blocking field** 1. Click on the unit conversation from the user. 2. **Click "Resolve"** in the upper right corner to close the conversation. _Note: This will give the contact a full response and indicate that the conversation is over. If the user re-enters the chat, "Resolve" = exits the "Closed" state and the new conversation will appear in the Teaminbox._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FYnOxcwjtU0fnkZ5l4OI99resolvee.png) 3. To view closed conversations, go to **"Status" -> "Resolved"** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F8VbwHBRffu2FFK1GoWZErstatusaa.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FwlxyvilwA0HWTzrGuvRzCstatusresolved.png) **Select "Employee" in the dialogue.** 1. Click on the unit conversation from the user. 2. **Selecting an Employee in the "Assign to: Member"** section in the upper left corner will indicate that the employee will respond or has responded to the conversation. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FSfRey2LEoFWZvxUpE0bkbmemberinsbo.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FMGrlqRmBQmcaadbtRlbaFchosemember.png) **Filter conversations** 1. Click on **the "3 lines"** in the upper left corner . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FCjdW10FrGDFINHKdaG2nlchosezuraas.png) 2. Select the field to filter from among the fields **Channel** , **Brand** , **Segment** , **Integration** , and **Tag .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FPwWIRG5xLrURNNB6wClArchoseintegration.png) 3. Clicking on the filter value will display the relevant conversations. _For example: If you search for Integration - Facebook messenger, only conversations from Facebook messenger will be displayed._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fc4GQfwkXzIPHtYOfzwrJJchosefbmesg.png) **Filter conversations by date** 1. **Click on the "Date"** section of the conversation . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fre2hYKlEj4PLvn1GV1NkNchosedate.png) 2. Enter the date and click the **"Filter"** button. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FeRjccxVXb3LMKJLBrRH8estartdatefil.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FeQribWviJaSIpPp0mIk9Benddatefil.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FFPloT2kyfyfFujHQVIn6pdatefilter.png) 3. Related conversations will be displayed. **Use canned responses in conversations** 1. Enter the keyword contained in the title or content of the pre-prepared answer. 2. Choose from the answers that appear, ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FF4xWmgjvYEXP4Ru5ichLIresponsetulhuurug.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FUTNenozif0ExUgTUeB0b6responsecontent.png) 3. Click the **Send button to send.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FCDqZVVSb9aQFsYf887NB8sendmesg.png) **View a user's previous conversation history** 1. Click on **"Activity" in the User area** on the right . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FpZsjm2DAcmHdsqOqe4kG7activity.png) 2. All conversations, including previous calls and messages, will be visible. **View customer feedback information** 1. **Click on "Related" in the User field** on the right . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FU9aYqT0fKmLPiboLN6BiBreleated.png) 2. Click on **Ticket .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FsVObPWpB7CDbfFe1MliYbrelatedticket.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FDr7e7oHgzkwsSxWfIlEIEreleatedcard.png) Related feedback information will be displayed. --- # 1. Registration and Payment Instructions Published 2025-04-30 > Description: Step-by-step guide for registering and purchasing a product through erxes.io: Steps: Visit erxes.io . Click the Sign Up button. Choose Email & Password as the sign-up method. Enter your email address and cre **Description:** Step-by-step guide for registering and purchasing a product through erxes.io: **Steps:** 1. Visit [erxes.io](https://erxes.io/). 2. Click the **Sign Up** button. 3. Choose **Email & Password** as the sign-up method. 4. Enter your email address and create a password, then click **Sign Up**. 5. Check your email inbox for a verification code and enter it in the required field. - _(If you did not receive the verification email, return to erxes.io and click **Sign In**.)_ 6. Go to the **Sign In** page. 7. Select **Email & Password** again. 8. Enter your credentials and click **Sign In**. 9. Click **Create New Organization**. 10. Enter your organization's name in English. _(Tip: Keep it short and simple.)_ 11. Click **Continue to Product Selection**. 12. Choose the product you wish to purchase. 13. Click **Choose a Plan**. 14. Select your preferred pricing package. 15. Carefully review your selections, then proceed to payment. 16. Enter your payment card details. - _(Note: Some banks may call you for payment confirmation. Even if you have sufficient funds, the process may take a few minutes.)_ 1. After entering your payment details, click **Subscribe**. 2. Once the payment is successful, click **Go to My Organization**. 3. When you see the main dashboard of the erxes application, your registration and payment are successfully completed.   1. Visit [erxes.io](https://erxes.io/). 2. Click the **Sign Up** button. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/3C6tiEwb3Hqalh1r97fHIa48090ef-aae3-4357-93e1-40ea23d4b423.png) 3. Choose **Email & Password** as the sign-up method. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/gaEJvRiojc2pEVqVHzkp2emailpass.png) 4. Enter your email address and create a password, then click **Sign Up**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/DLRAFuiQ1nDMsYmsOglIrsignup.png) 5. Check your email inbox for a verification code and enter it in the required field. _(If you did not receive the verification email, return to erxes.io and click **Sign In**.)_ Go to the **Sign In** page. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/dLjmeYTAJbDUhXSsxZKuWsignin.png) 6. Select **Email & Password** again. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/4hCFLigNfu2u1yC4DP0Bzsinngn.png) 7. Enter your credentials and click **Sign In**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/uej9YlbdKpazM6wPHx8Rssinginnn.png) 8. Click **Create New Organization**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/ZVCJ9lYr78Foe75rPsq0ocreateorg.png) 9. Enter your organization's name in English. _(Tip: Keep it short and simple.)_ 10. Click **Continue to Product Selection**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/M641FQ6irHSUA6AvevmSVorgname.png) 11. Choose the product you wish to purchase. 12. Click **Choose a Plan**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/sNQUq9oJWg5zKxc6AGEaPchooseprd.png) 13. Select your preferred pricing package. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/4WcHpizQYad8lfFpIKiCCpsioa.png) 14. **Carefully review your selections, then proceed to payment.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/pdzsMCOvDcA3Xe54hMaxWreviewandfinish.png) 15. Enter your payment card details. 16. _(Note: Some banks may call you for payment confirmation. Even if you have sufficient funds, the process may take a few minutes.)_ 17. After entering your payment details, click **Subscribe**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/h3ZsjNtpzCatrQrXoaCAtsubscribe.png) 18. Once the payment is successful, click **Go to My Organization**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/7WnKMPzffueA8XlY1AdQcgotoorg.png) 19. When you see the main dashboard of the erxes application, your registration and payment are successfully completed. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/kVlF65kWsXwSI1WhB72mOerxesund.png) --- # 1. Registration and Payment Instructions Published 2025-04-30 > Description: Step-by-step guide for registering and purchasing a product through erxes.io: Steps: Visit erxes.io . Click the Sign Up button. Choose Email & Password as the sign-up method. Enter your email address and cre **Description:** Step-by-step guide for registering and purchasing a product through erxes.io: **Steps:** 1. Visit [erxes.io](https://erxes.io/). 2. Click the **Sign Up** button. 3. Choose **Email & Password** as the sign-up method. 4. Enter your email address and create a password, then click **Sign Up**. 5. Check your email inbox for a verification code and enter it in the required field. - _(If you did not receive the verification email, return to erxes.io and click **Sign In**.)_ 6. Go to the **Sign In** page. 7. Select **Email & Password** again. 8. Enter your credentials and click **Sign In**. 9. Click **Create New Organization**. 10. Enter your organization's name in English. _(Tip: Keep it short and simple.)_ 11. Click **Continue to Product Selection**. 12. Choose the product you wish to purchase. 13. Click **Choose a Plan**. 14. Select your preferred pricing package. 15. Carefully review your selections, then proceed to payment. 16. Enter your payment card details. - _(Note: Some banks may call you for payment confirmation. Even if you have sufficient funds, the process may take a few minutes.)_ 1. After entering your payment details, click **Subscribe**. 2. Once the payment is successful, click **Go to My Organization**. 3. When you see the main dashboard of the erxes application, your registration and payment are successfully completed.   1. Visit [erxes.io](https://erxes.io/). 2. Click the **Sign Up** button. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/3C6tiEwb3Hqalh1r97fHIa48090ef-aae3-4357-93e1-40ea23d4b423.png) 3. Choose **Email & Password** as the sign-up method. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/gaEJvRiojc2pEVqVHzkp2emailpass.png) 4. Enter your email address and create a password, then click **Sign Up**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/DLRAFuiQ1nDMsYmsOglIrsignup.png) 5. Check your email inbox for a verification code and enter it in the required field. _(If you did not receive the verification email, return to erxes.io and click **Sign In**.)_ Go to the **Sign In** page. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/dLjmeYTAJbDUhXSsxZKuWsignin.png) 6. Select **Email & Password** again. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/4hCFLigNfu2u1yC4DP0Bzsinngn.png) 7. Enter your credentials and click **Sign In**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/uej9YlbdKpazM6wPHx8Rssinginnn.png) 8. Click **Create New Organization**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/ZVCJ9lYr78Foe75rPsq0ocreateorg.png) 9. Enter your organization's name in English. _(Tip: Keep it short and simple.)_ 10. Click **Continue to Product Selection**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/M641FQ6irHSUA6AvevmSVorgname.png) 11. Choose the product you wish to purchase. 12. Click **Choose a Plan**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/sNQUq9oJWg5zKxc6AGEaPchooseprd.png) 13. Select your preferred pricing package. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/4WcHpizQYad8lfFpIKiCCpsioa.png) 14. **Carefully review your selections, then proceed to payment.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/pdzsMCOvDcA3Xe54hMaxWreviewandfinish.png) 15. Enter your payment card details. 16. _(Note: Some banks may call you for payment confirmation. Even if you have sufficient funds, the process may take a few minutes.)_ 17. After entering your payment details, click **Subscribe**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/h3ZsjNtpzCatrQrXoaCAtsubscribe.png) 18. Once the payment is successful, click **Go to My Organization**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/7WnKMPzffueA8XlY1AdQcgotoorg.png) 19. When you see the main dashboard of the erxes application, your registration and payment are successfully completed. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/kVlF65kWsXwSI1WhB72mOerxesund.png) --- # 9. Integration with Sales and Frontline Modules Published 2025-04-30 > Integration with Sales and Frontline Modules Visit erxes.io and navigate to the Guide section. Select the relevant product, such as Sales or Frontline , from the product guide options. Follow the detailed instructions pr **Integration with Sales and Frontline Modules** 1. **Visit erxes.io** and navigate to the **Guide** section. 2. Select the relevant product, such as **Sales** or **Frontline**, from the product guide options. 3. Follow the detailed instructions provided on the page to integrate these modules into your erxes system. - In the **Sales** guide, you'll find steps to connect the **sales pipeline**, set up **lead management**, and optimize **reporting**. - For **Frontline**, the guide will show how to integrate **customer interaction data**, track **performance metrics**, and connect with other modules. 4. Adjust any settings as needed to tailor the integration to your specific workflow. 💡 **Things to note:** - **Sales** and **Frontline** modules can be seamlessly integrated with other erxes tools to enhance productivity and data flow. - Refer to the **Sales** or **Frontline** product guides on erxes.io for step-by-step instructions. --- # 8. Operations Dashboard Overview Published 2025-04-30 > Creating a Dashboard Go to the main page of erxes and select Insight from the menu. Click the Add Dashboard button next to the Dashboard name field (the button marked with a (+) symbol). In the Dashboard Name field, ente **Creating a Dashboard** 1. **Go to the main page of erxes** and select **Insight** from the menu. 2. Click the **Add Dashboard** button next to the Dashboard name field (the button marked with a (+) symbol). 3. In the **Dashboard Name** field, enter a name for your dashboard (e.g., "Sales"). 4. In the **Choose Section** field, type **"Task"** and click **Create**. 5. Check the box for **Task Chart** to add it to your dashboard. 6. Click **Save** to save your new dashboard. --- # 7. Customizing Tasks with Additional Fields Published 2025-04-30 > Hello, you can add a new field to the Main Action field or Task by following the instructions below. Add a field to a Task: Go to Settings Select Properties Select Tasks. Add Group: Click Add Group & Field Select Add Gro Hello, you can add a new field to the Main Action field or Task by following the instructions below.  - Add a field to a Task:  1. Go to **Settings** 2. Select **Properties** 3. Select **Tasks.** **Add Group:** 1. Click **Add Group & Field** 2. Select **Add Group** and fill in the fields below: 1. **Name**: Primary Group Name 2. **Description**: Description 3. **Boards & Pipelines**: Select the Boards & Pipelines in the Task pane you created. You can select multiple Boards & Pipelines here. If you don't select them, you may encounter issues with visibility of the properties you created. 4. **Always open**: Option to keep the field open whenever you enter the task card. 3. Click the **Save** button to save. **Add Fields:** Click **Add Group & Field** again. 1. Select **Add Property** and fill in the fields below: 1. **Name**: Specifies the name of the field to be created. 2. **Group**: Select the previously created group name. 3. **Type**: Select the type of field to fill. 4. **Show in card**: The information will be visible outside the Properties task card you created. This will be the same as the information that appears outside the card when you select Company or Customer. 2. Click the **Save** button to save. **Edit the Task Field:** 1. **Go to Settings** 2. **Select Properties** 3. **Select Tasks** 4. **Edit Group Name**: Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save** button to save. 6. **Edit Property Name**: Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save** button to save. **Delete the Task Field:** 1. **Go to Settings** 2. **Select Properties** 3. **Select Tasks** 4. **Choose Group Name**: Select the group name to delete. 5. Click the **Delete** button to delete. 6. **Choose Property Name**: Select the name of the field to delete. 7. Click the **Delete** button to delete.fill in the fields below. - _**Name**_ : Primary Group Name - _**Description**_ : Description - _**Boards & Pipelines**_ : Select the Boards & pipelines in the Task pane you created. You can select multiple Boards & pipelines here. If you don't select them, you will encounter the problem of not seeing the Properties you created. - _**Always open**_ : Option to keep the field open whenever you enter the task card. 8. Click the **Save button to save.** 9. Click **Add Group & Field** again. 10. **Select Add Property** and fill in the fields below. - _**Name**_ : Specifies the name of the field to be created. - _**Group**_ : Selectthe previously created _Group name ._ - _**Type**_ : Select the type of field to fill.  11. **Show in card** : The information will be visible outside the Properties task card you created. This will be the same as the information that appears outside the card when you select Company or Customer. 12. Click the **Save button to save.**   **Edit the Task field:**  1. Go to **Settings** 2. Select **Properties** 3. Select **Tasks .** 4. **Edit Group name** : Select the group name you want to edit and click the edit button to enable editing. 5. Click the **Save button to save.** 6. **Edit Property name** : Select the name of the field you want to edit and click the edit button to enable editing. 7. Click the **Save button to save.**   **Delete the Task field:**  1. Go to **Settings** 2. Select **Properties** 3. Select **Tasks .** 4. **Choose Group name** : Select the group name to delete. 5. Click the **Delete button to delete.** 6. **Choose Property name** : Select the name of the field to delete. 7. Click the **Delete button to delete.** --- # 6. Setting Task Access Permissions Published 2025-04-30 > Setting Task Access Permissions Go to Task Pipeline Settings Find and click on the Task Pipeline you want to edit Under the settings, locate the option Show only the user's assigned (created) task Check the box next to S **Setting Task Access Permissions** 1. **Go to Task Pipeline Settings** 2. **Find and click on the Task Pipeline** you want to edit 3. Under the settings, locate the option **Show only the user's assigned (created) task** 4. **Check the box** next to **Show only the user's assigned (created) task**. This will ensure that users can only see tasks that are assigned to them. 5. In the section labeled **Users eligible to see all tasks**, select the users who should have access to view all tasks. Typically, **Admin** or users with higher permissions can be selected here to see all tasks across the platform. 6. Click **Save** to apply the changes 💡 **Things to note:** - When the **Show only the user's assigned task** option is enabled, each user will only be able to view their own tasks. - **Admin users** or users with higher roles can be granted permission to view all tasks, ensuring they have full oversight. --- # 5. Using Calendar and Setting Reminders Published 2025-04-30 > Go to Task Settings Open the Task you want to set a Close Date for In the task details, locate the Close Date field Click on the Close Date field to select a date from the calendar or manually type the date Set Reminders 1. **Go to Task Settings** 2. **Open the Task** you want to set a **Close Date** for 3. In the task details, locate the **Close Date** field 4. **Click on the Close Date field** to select a date from the calendar or manually type the date **Set Reminders for Close Date** 1. Once the **Close Date** is set, click on **Set Reminders** below the Close Date field 2. In the reminder settings, choose the desired time before the task's **Close Date** to receive a reminder. Options include: - 1 hour before - 1 day before - 2 days before - Or other available time intervals 3. Select your preferred reminder time and click **Save** 💡 **Things to note:** - Setting reminders will send you an email notification about the task based on the selected time interval. - You can set multiple reminders for each task to ensure timely completion. **Get Notification by Email Config** 1. **Go to Settings** 2. **Select Notification config** 3. **Click on the Notification bar** and scroll down 4. **Get notification by email**: Check and click **Save** --- # 4. Import Tasks Information Published 2025-04-30 > Go to the Settings menu Go to Import & Export and select Import Enter the Task by clicking the Next button Click on Download template to download the Excel template and enter your information Download as a CSV file Enter - **Go to the Settings menu** - **Go to Import & Export** and select **Import** - **Enter the Task** by clicking the **Next** button - **Click on Download template** to download the Excel template and enter your information - **Download as a CSV file** - **Enter the Choose a file to upload your cards**: task and click **Next** - In the **Select Property** section, match the header column with the **erxes** header information and give it a Name - **Check and save the CheckDisclaimer** - **Check the results** to see if the import was successful in the **See Records** result section --- # 3. Create a Task Process Published 2025-04-30 > Go to Settings Select Task Process from the menu Click Add New Board , type "Tasks," and click Save Click Add New Process (Note: This is where you can define and manage task statuses and steps.) In the Name field, type " 1. **Go to Settings** 2. **Select Task Process from the menu** 3. **Click Add New Board**, type "Tasks," and click **Save** 4. **Click Add New Process** _(Note: This is where you can define and manage task statuses and steps.)_ 5. In the **Name** field, type "Task Workflow" 6. **Click Add Another Stage** and type "To Do" in the **Stage Name** field 7. **Click Add Another Stage** and type "In Progress" in the **Stage Name** field 8. **Click Add Another Stage** and type "Under Review" in the **Stage Name** field 9. **Click Add Another Stage** and type "Completed" in the **Stage Name** field 10. **Click Add Another Stage** and type "Archived" in the **Stage Name** field 💡 **Things to note**: - You can create as many stages as needed, depending on your workflow. - Avoid unnecessary complexity — keep your stages meaningful and minimal. - You can customize each task stage with labels, deadlines, and assignees. --- # Creating and Adding Custom Fields Published 2025-04-29 > This guide explains the steps to create custom fields for a plugin in erxes, specifically for users, and how to add additional properties. Instructions Step 1. Go to the Settings section Click on the Properties option. S 1. This guide explains the steps to create custom fields for a plugin in erxes, specifically for users, and how to add additional properties. 2. ### **Instructions** **Step 1. Go to the Settings section** 1. Click on the **Properties** option. **Step 2. Add a New Group** 1. On the left side of the field, you will see a variety of plugins. For example, to create additional data for the **Customers** user, click **Add Group & Field**. 2. **Create an Add Group**: 1. Click the **Add Group** button. 2. In the **Name** field, enter the Primary Group name. 3. In the **Description** section, enter a description of the group. 4. In the **Always Open** section, select whether the field will be open every time you enter the task card. 5. Click the **Save** button to save. **Step 3. Create Add Property** 1. **Create Add Property**: 1. Click the **Add Group & Field** button again. 2. Click the **Add Property** button. 3. In the **Name** field, enter the name of the field to be created. 4. In the **Group** section, select the group name you created earlier. 5. In the **Type** section, select the type of field to fill in (e.g., text, option, date, etc.). 6. Click the **Save** button to save. **Step 4. Check the field you created** 1. Go to the **Customer** section under the **Contacts** menu. 2. Refresh and check the newly created field. **Step 5. Adding Fields in Other Plugins** 1. An additional field will appear on the right side of erxes' **Sales Pipeline** card-type plugins. 2. **Contacts-type plugins** have an additional field on the left side. 3. Field types can be: 1. **Checklist** (Select as a list) 2. **Radiobutton** (Make a single selection) 4. ### **Things to Note:** 5. ✔ After setting all fields correctly, you need to click the **Save** button and check your changes. 6. ✔ If the settings change is not immediately visible, you can refresh the **Contacts** section to see the new fields. 7. ✔ Selecting the appropriate field will display the information more efficiently. --- # Categorizing Conversations and Generating Reports Published 2025-04-29 > By categorizing conversations using tags and generating reports, you can track and analyze user issues. Instructions Step 1. Create a conversation category Go to Settings . Click on the Tag menu. Select a conversation ty By categorizing conversations using tags and generating reports, you can track and analyze user issues. ### **Instructions** **Step 1. Create a conversation category** - Go to **Settings**. - Click on the **Tag** menu. - Select a **conversation type**. - Click the **Add tag** button. - In the **Name** field, type the name of the category. - Click the **Save** button to save. **Step 2. Select a category for the conversation** - Go to **Team Inbox**. - Click on **No tag**. - Select the category you created. **Step 3. Generate reports by conversation category** - Go to the **Insight** menu. - Click the **Add chart** button. - In the **Service** section, select **Inbox**. - In the **Chart Template** section, select **Total conversation count**. - In the **Select dimension** section, select **Tag**. - In the **Chart type** section, select **Table**. - Click the **Save** button to save.   **Illustrated instructions** **1\. Create a conversation category** 1. Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Click on the **Tag menu** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FTCgmsnVc3RR0a6Y40zjVjtagsettings.png) 3. Select **a conversation type.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FCCs1g3lfugGGf7AVYGu3Ftagconversation.png) 4. Click the **Add tag button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fn9uAtcMfr0jJQNeLqq5MZtagaddtag.png) 5. **In the Name** field, type the name of the category. 6. Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FQWyKwkUjE1FT81cKwcfvXtagadd.png) **2\. Select a category for the conversation** 1. Go to **Team Inbox** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FRJrV5TugwUHnWb2231nSMtaginboxchose.png) 2. Click on **No tag.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F6AEDhpKSnF2acwhodAxAFinboxnotag.png) 3. **Select the category** you created. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fj4r7uL0udHY7EUJULLD4winboxtaglah.png) **3\. Generate reports by conversation category** 1. Go to the **Insight menu** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FkrgB3wb3xX93zGxMDjmBUtaginsight.png) 2. Click the **Add chart button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FehIhziEgiEDUNqCXvjpNOtagaddchart.png) 3. **In the Service** section, select **Inbox .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzkVHgq32YV4is4Z4IsnPwtagchoseinbox.png) 4. **In the Chart Template** section, select **Total conversation count .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FD1V52eAckCBLtEHpqHhv3tagchosecount.png) 5. **In the Select dimension** section , select **Tag .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzJ2ksPnoj47VJJGCVvagztagchosedimention.png) 6. **In the Chart type** section , select **Table .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FavzNfN0lu8Wy1fYLxmKSDtagchosetable.png) Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FHVzT5FQHKLm0BIijYd4VRtaginsightsave.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FNkxgtbeCuGjRYqYSnf4m7taglastinsight.png) --- # Creating and Placing a Pop-up Form for Customer Feedback Published 2025-04-29 > This guide will explain the process of creating and placing a pop-up form on a website to collect customer feedback. Instructions 1. Prepare the form content Prepare the names of the fields that will be filled in the for This guide will explain the process of creating and placing a pop-up form on a website to collect customer feedback. ### **Instructions** **1\. Prepare the form content** Prepare the names of the fields that will be filled in the form. For example: - Name - Phone - Email - Feedback **2\. Create a form** - Go to the **Form** menu. - Click on the **Lead Generation** tab. - Click the **Create Lead** button. - Select **Pop-Up** and click **Next**. - Check the **Skip CallOut** box and click **Next**. - In the **Form Title** field, type **"Feedback"**. **3\. Add fields** - **Name**: - Click on **First Name**. - In the **Field Label** section, type **"Name"**. - Click the **Add to Form** button to add it. - **Phone**: - Click on **Phone**. - In the **Field Label** section, type **"Phone"**. - Click the **Add to Form** button to add it. - **Email**: - Click on **Email**. - In the **Field Label** section, type **"Email"**. - Click the **Add to Form** button to add it. - **Feedback**: - Click on the **text area**. - In the **Field Label** section, write **"Please provide feedback"**. - Click the **Add to Form** button to add it. **4\. Form settings** - Go to the **Options** section. - **Form name** – Give it a name like **"Feedback"**. - Select a **brand**. - Select a **channel**. - Click the **Save** button to save the form. ### **Test and Check** - Click the **"< >" Install code** button in the Actions section behind the form. - Click the **Preview** button to see the form. - A form will appear with **Name**, **Phone**, **Email**, and **Feedback** fields visible. - Fill in the information and click the **Send** button to try it out. ### **⚠️ If the form is not visible:** - Check if the form's **Install code** has been copied and pasted correctly. - Check if the **Brand/Channel** settings are correct. - Check if the form **status** is active. This process will allow you to create a customer feedback form that is easy to integrate into your website as a pop-up, making it simple for users to provide valuable feedback. **Illustrated instructions** **1\. Prepare the form content** Prepare the names of the fields that will be filled in the form. For example: - Name - Phone - Email - Feedback **2\. Create a form** - Go to the **Form menu** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fn4lXvoIKNjmxsJLAgWG7Wformmms.png) - Click on **the Lead Generation tab.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhyHeCP0rWaqnujLAw5iNNformleadgen.png) - Click the **Create Lead button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FKGhYD7GWUC9L3hwzL427jformcreatelead.png) - Select **Pop-Up** and click **Next.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FTMXCq6CAVAZy4FGeOaoHYformpopup.png) - Check the **Skip CallOut** box and click **Next.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FLItsZMK7m8GhpNwevFE9eformskippp.png) - **In the Form Title** field, type “Feedback” ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FvDl8wsXHuN5vWVRPLdbs0formtitle.png) **3\. Add fields** - Name: - Click on **First Name.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FeHNdKsD02ZT1JnQOz8wcwformnamechose.png) - **In the Field Label** section, type “Name.” - Click the **Add to Form button to add it.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzhsFIHP54dh3PjF1ZepVwformnamead.png) - Phone: - Click on **Phone** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7ORpGc9M5OiY5mZdNGnvqformphonechose.png) - **In the Field Label** section, type “Phone” - Click the **Add to Form button to add it.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FOX5kf2wPjbLslNl5a6RgMformphoneadd.png) - Email: - Click on **Email.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FSISb6d7yfv1Shwc1dkAnWformemailchose.png) - **In the Field Label** section, type “Email” - **Click the Add to Form button to add it.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FYuJaghkQozBgwmWPzw4TLformemailadd.png) - Feedback: - Click on **the text area.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FsX6c5GrLXxajYD3VK1691formtextareachose.png) - **In the Field Label** section, write “Please provide feedback.” - Click the **Add to Form button to add it.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FfJKX86r3adyD2nnAgNrp1formtextareaadd.png) **4\. Form settings** - Go to **the Options section.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FjJyF3rtatWNhZSLbrFGXyformsavebefore.png) - **Form name** – Give it a name like “Feedback” ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F49kp3k8V9M9vPCMZWhn8kformname.png) - Select **a brand** - Select **Channel** - Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzCPPJ2fgQB00a9U06Ufbgformsave.png) **Test and check** - Click the **"< >” Install code** button in the Actions section behind the form. - Click **the Preview button to see.** - A form will appear with Name, Phone, Email, and Feedback fields visible. - Fill in the information and click **the Send button to try it out.** **⚠️ If the form is not visible:** - Check if the form's Install code has been copied and pasted correctly. - Check if the Brand/Channel settings are correct. - Check if the form status is active. --- # Creating a Knowledge Base and Client Portal on the erxes System Published 2025-04-29 > This guide outlines the steps for creating a Knowledge Base with frequently asked questions (FAQs) and answers for users on the erxes system, as well as setting up a Client Portal to share it with the public. By organizi This guide outlines the steps for creating a Knowledge Base with frequently asked questions (FAQs) and answers for users on the erxes system, as well as setting up a Client Portal to share it with the public. By organizing and posting instructions, answers, and articles related to your products and services, users can solve their problems independently. Distributing this knowledge base through a public portal can reduce the burden on support services, improve user experience, and resolve common issues faster. **Step 1. Create a Knowledge Base** **Question:** How to create a knowledge base? **Answer:** - Go to **Knowledge Base**. - Click on **Add Knowledge Base**. - Enter a name in the **Title** field. - Select a **brand**. - Enter a code (example: **helpcenter**). - Click the **Save** button to save the knowledge base. - Click the ⚙️ settings icon next to the name you created. - Click **Add Category** to create a category: - **Title**: Category name (e.g., **Settings**). - **Code**: Any code (e.g., **setup**). - **Icon**: Select an icon. - Click the **Save** button. - When adding a subcategory: - Click **Add Category** again. - In the **Parent category** section, select the category you created earlier. - Fill in the other details and click **Save**. **Step 2. Add Article (Content)** **Question:** How do I submit a new article? **Answer:** - Select a **Child Category** in **Knowledge Base**. - Click the **Add Article** button. - **Title**: Content name (e.g., **Login Instructions**). - **Code**: Any code (e.g., **login-guide**). - **Reaction**: Set the user response settings. - **Status**: Select **Publish**. - **Content**: Write the article content. - Click the **Save** button to save the article. **Step 3. Create a Client Portal** **Question:** How to create a Client Portal? **Answer:** - Go to **Settings**. - Select **Business Portal** > **Client Portal**. - Click **Add New Client Portal**. - **Portal name**: Help. - **Website**: https://companyname.clientportal.erxes.io/help. - **KnowledgeBase**: Check this option. - **Description**: Information portal. - **Knowledge Base Topic**: Select a previously created knowledge base. - Go to the **Appearance** section. - **Header/Footer**: Enter company colors. - Click the **Submit** button to save the Client Portal. This guide helps you create a functional Knowledge Base and Client Portal on erxes, making it easier for users to find answers, improving overall service efficiency. **ILLUSTRATED INSTRUCTIONS** **1\. Instructions for creating a Knowledge Base** 1. Go to **the Knowledge Base .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F2mmq5IWSGFHSlbLOScG0Hchoseknowledgebase.png) 2. Click the **Add Knowledge Base button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FiM4EIbG1Qq3vu1zkxjX0Bcpaddknowledgebsa.png) - Enter your name in **the Title field.** - **Select a brand** . - Enter **the code** (example: helpcenter ). - Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FfUeVD7lt4bU6tgZXT7rVocpsaveknowledgebase.png) 3. Click on **the ⚙️ settings icon** next to the name you created . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhghwqaCmHy72bCOvQqsRPcpchosetohirgoo.png) 4. Click the **Add Category button to add a category:** - **Title** : Category name (e.g. Settings) - **Code** : any code (e.g. setup ) - **Icon** : select - Click the **Save button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F2YPeaRCPobs77zA0bxJnbcpsaveparent.png) 5. When adding a new **subcategory** : - Click the **Add Category button again.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhghwqaCmHy72bCOvQqsRPcpchosetohirgoo.png) - **In the Parent category** section, select the category you created earlier. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FuSUDFgVikgAz7fgukevcycpchosechildparent.png) - Fill in the other information and click **Save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FqkFtFCF9AdsUtwevYzXehcpsavechild.png) **2\. Add Article (Content)** 1. Select **Child Category** in **Knowledge Base** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FNsjTlPpEj0XAyeYwsW1s3cpchosechild.png) 2. Click the **Add Article button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FrX0PwvdA291P63eOFGJZgcpaddarticle.png) - **Title** : Content name (e.g. Login Instructions) - **Code** : any code (example: login-guide ) - **Reaction** : user response settings - **Status** : Select Publish. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Frmcq6E2pdLsVxnVbz1OyRcparticlepublish.png) - **Content** : Write the content. 3. Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FoG7ah8nJ3H7bmoDCsPCexcparticlesave.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FKWzDIVazx1m5f2upiWzYGcparticleview.png) **3\. Create a Client Portal** 1. Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fc5fPwbfk3fouhqT2eEExpcpsettings.png) 2. Select **Business Portal > Client Portal .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FNnUr7xAGfxYJGdgv3dNr9cpbusinessportal.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FLgbSvm09kKx8ac0fwdYincpchoseclientportal.png) 3. Click **Add New Client Portal.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F3wCNGB0rYqQxv4pLPn6grcpaddclientportal.png) - **Portal name**: Help - **Website**: https://companyname.clientportal.erxes.io/help - **KnowledgeBase** : check - **Description** : Information portal - **Knowledge base Topic** : select a previously created knowledge base ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FxEcrV6FK0E0DOhUHsMTzYcpchosekb.png) 4. Go to **the Appearance section.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fjb5iRnesP7fuYjdwTen2Ncpappearance.png) - **Header/Footer** : enter company colors ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FsiaFAfCwSfqtBAt6DEKy5cpheaderfoot.png) 5. Click the **Submit button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FlZdflEnNelH7dQM8pGNuocpsave.png) --- # Creating and Using Response Templates Published 2025-04-29 > Responses, or canned response templates, allow you to quickly send messages with the same content that you frequently send to your users. This saves users time and helps standardize service quality. Instructions : Go to Responses, or canned response templates, allow you to quickly send messages with the same content that you frequently send to your users. This saves users time and helps standardize service quality. **Instructions**: 1. Go to the **Settings** section. 2. Click on **Responses**. 3. Click the **New response template** button to create a new template. 4. Enter the following information: - **Brand** – Choose the brand that the template will be used for. - **Name** – Enter a name for the response template (choose a name that is easy to identify internally). - **Content** – Enter the message content to be sent (can be a combination of HTML and text). 5. Click the **Save** button to save the template. **Try it out**: Using a Pre-made Template 1. Go to **Team Inbox**. 2. Click to enter a conversation with a contact. 3. Use your responses in one of two ways: - Search by typing the response keyword. - Click the **Response Template** button to the left of the **Send** button in the lower-right corner. 4. Choose the correct response. 5. Click the **Send** button to send the message to the user. --- # Connecting IMAP Published 2025-04-29 > Step 1: Add the IMAP Integration Go to Settings . Navigate to Integration . In the IMAP section, click +Add . Step 2: Fill in the IMAP Information Email Address : Enter the email address you want to connect. Host : Enter ### **Step 1: Add the IMAP Integration** 1. **Go to Settings**. 2. Navigate to **Integration**. 3. In the **IMAP** section, click **+Add**. ### **Step 2: Fill in the IMAP Information** 1. **Email Address**: - Enter the email address you want to connect. 2. **Host**: - Enter the IMAP server host. For example, for Gmail: **imap.gmail.com.** 3. **SMTP Host**: - Enter the SMTP server host. For example, for Gmail: **smtp.gmail.com.** 4. **SMTP Port**: - Usually, the SMTP port is **465**. 5. **Main User**: - Enter your email address. 6. **User**: - Enter your email address again. 7. **Password**: - To generate an app-specific password for Gmail, follow these steps: a. Go to **My Profile**. b. Click **Manage Account**. c. Select **Security**. d. Enable **2-Step Verification** (if not already enabled). e. Confirm your phone number. f. Click on the “**\>**” arrow next to **App Password**. g. In the **App Name** field, type "erxes". h. Click **Create**. i. Copy the generated **App Password**. - **Enter the app password** you copied into the **Password** field on the erxes integration page. 8. **Brand**: - Select the brand you want to connect to. 9. **Channel**: - Select the channel you want to connect to. 10. Click **Save** to save the settings. ### **Step 3: Troubleshooting (If You Don’t See App Password)** 1. Ensure **2-Step Verification** is enabled. 2. If **App Password** is not appearing, double-check that the required fields for **2-Step Verification** are correctly filled out. By following these instructions, you’ll be able to connect your IMAP email to the erxes platform, allowing your team to track incoming and outgoing emails and manage communication seamlessly from the Team Inbox. **Illustrated instructions:** 1. Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click **+Add in the IMAP** section . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FJR3DqHrh5pRYZ8BpNawRAimapadd.png) 4. Fill in the information below: - **Email address** – The email address you are trying to connect to. - **Host** – For example: [imap.gmail.com](http://imap.gmail.com/) - **SMTP host** – For example: [smtp.gmail.com](http://smtp.gmail.com/) - **SMTP port** – Usually 465 - **Main user** – Enter your email address - **User – Enter your email address** - **Password –** [**Follow the app password creation guide**](https://support.google.com/accounts/answer/185833?hl=en) below. - Go to **My profile** - Click on **Manage account.** - Click on **Security.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FihPxCqLNAeMcP8Id1TSdqimapsecurity.png) - Click on **2-Step verification.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fm5KBy1iXPzLCcywHOMQ5Cimap2stepveri.png) - Confirm **your phone number .** - Click on the **“>”** arrow next to **App Password .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FFVBl906iH6jolfiCnsQuVimapapppass.png) - In **the App name** field, type **“erxes”** - Click on the **Create button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F114i6MMumolKVG8K0XLMXimappasscreate.png) - Copy **the “Password”** that appears. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FrM6Nqt7ix31Hy0Ihq52Iiimappassdone.png) - Enter **the password you copied** in the erxes Integration **“Password”** field . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FkvH2IalFYyQnRek5Rz2F2imappastpass.png) - **Brand** – Select the brand to connect to. - **Channel** – Select the channel to connect to. 5. Click the **Save button to save the settings.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FUUWEbRr6whqNHvSdU6sGFimapsavee.png) **If you don't see App Password, check the following:** - **Click on 2-Step verification** and verify the required fields. --- # Connecting Viber Published 2025-04-29 > Step 1: Add the Viber Channel Go to Settings . Navigate to Integration . In the Viber section, click +Add . Name : Enter a name for your Viber channel (e.g., "[Viber] Business Support"). Token : In the Token section, ent ### **Step 1: Add the Viber Channel** 1. **Go to Settings**. 2. Navigate to **Integration**. 3. In the **Viber** section, click **+Add**. 4. **Name**: - Enter a name for your Viber channel (e.g., "\[Viber\] Business Support"). 5. **Token**: - In the **Token** section, enter the Viber bot token information. - If your Viber bot is not created yet, you can create it by visiting [Viber's Partner Portal](https://partners.viber.com/). - After creating the bot, you’ll receive the **Token** information. Copy it and paste it into this section. 6. **Brand**: - Select the brand that you want to connect to. 7. **Channel**: - Select the channel through which you will communicate. 8. Click **Save** to save the settings. ### **Step 2: Test the Viber Bot** 1. Open the **Viber app** and send a message to the bot you created. 2. Go to the **erxes Team Inbox window**. 3. Check if the message you sent is arriving in **TeamInbox**. 4. Write a **reply** and confirm that the message successfully reaches the user on their Viber app. ### **Step 3: Troubleshooting (If Viber Messages are Not Appearing in TeamInbox)** #### 1\. **Check if your erxes account is included in the channel** - Go to **Settings → Channel**. - Check if your **erxes account name** is listed in the **Member** section. #### 2\. **Ensure Channel is selected for Viber connection** - Go to **Settings → Integration**. - Click on the **Viber section**. - Click the **Edit** button and make sure the **Channel** is selected. This tutorial explains step by step how to connect a Viber channel on the erxes platform. Connecting a Viber channel will allow you to communicate with your users directly via messenger. **Instructions with pictures:** 1. Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click **+Add in the Viber** section . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F9XvSGUasUa8K6PeiRfNjmviberadd.png) 4. **In the Name** field, enter the name of the Viber channel you want to connect to _(for example: "\[Viber\] Business Support")_ 5. Enter the Viber bot token information in **the Token section.** - If your Viber bot is not yet created, you can create one by visiting the following link: [**https://partners.viber.com/**](https://partners.viber.com/) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FBMzjqh8yDs1j0RFEJzqmEviberlink.png) - After creating the bot, **the Token** information will appear, copy it and enter it in this section. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FAxguHwOucmELeQhiyWOShvibercopytoken.png) 6. **Brand** – Select the brand to connect to. 7. **Channel** – Select the channel to communicate with. 8. Click the **Save button to save the settings.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FEqKFfDctx8kxFMHMqZ6BPvibersave.png) **Try it:** 1. The user will test the bot by sending a message through their Viber app. 2. Go to the **erxes Team Inbox window** 3. Check if the sent message is arriving in **TeamInbox** 4. Write a reply and confirm that the message has been sent to the user. --- # Connecting Web Messenger Published 2025-04-29 > Step 1: Add and Configure Web Messenger Go to Settings . Navigate to Integration . In the Messenger section, click +Add . Appearance section : Enter your branding colors for the messenger to match your website's theme. G ### **Step 1: Add and Configure Web Messenger** 1. **Go to Settings**. 2. Navigate to **Integration**. 3. In the **Messenger** section, click **+Add**. 4. **Appearance section**: - Enter your branding colors for the messenger to match your website's theme. 5. **Greeting section**: - Select the information and customer service team members that will appear when a customer first enters the messenger. 6. **Intro section**: - Configure the message that will appear to offline users when they initiate a chat while the support team is unavailable. 7. **Hours & Availability section**: - Set your working hours to specify when your support team is available to respond in the messenger. 8. **Config Setup section**: - In the **Integration name** field, enter \[Web messenger\] Website. - Select the **Brand** section. - If you haven’t created a brand, click **Create Brand** and select it. - Select the **Channel** section. - If you haven’t created a channel, click **Create Channel** and select it. 9. Click **Save** to save the settings. ### **Step 2: Test the Web Messenger** 1. **Go to Settings**. 2. Navigate to **Integration**. 3. Click on **Messenger**. 4. Click on the **"<>”** icon from the **Actions** menu next to the Web Messenger name. 5. Click **Preview**. 6. In the window that appears, click on the chat circle in the lower right corner to start writing a chat. - Test if the messenger responds correctly based on the configured settings. ### **Step 3: Troubleshooting (If Web Messenger is not Coming to TeamInbox)** #### 1\. **Check if your erxes account is included in the Channel** - Go to **Settings → Channel**. - Check if your **erxes account name** is listed in the **Member** section. #### 2\. **Ensure Channel is selected for Web Messenger connection** - Go to **Settings → Integration**. - Click on **Web Messenger**. - Click **Edit** and ensure the **Channel** is selected. By following these steps, you should have Web Messenger properly connected and working in your erxes system, allowing your team to manage and respond to customer communications in real time. **Illustrated instructions:** 1. Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click **+Add in the Messenger** section . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FrqfImclGHIbCTEeAihfrQmsgadd.png) 4. **In the Appearance** section, enter information such as branding colors. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F33sivQFo4KhA6KIsYivgrmsgappearance.png) 5. **In the Greeting** section, select the information and customer service team members that will appear when the customer first enters the messenger. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FIpIzEXb4YPbwMvoBuGjanmsggreeting.png) 6. Configure the message that appears in **the Intro section to offline users.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F6zS2rJR3cVJAd2edTSuLlmsgintro.png) 7. You can set your working hours in **the Hours & Availability section.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F2A243KozNUfJXCmIO1mlQmsghour.png) 8. **In the Config Setup** section: - In the Integration name field, enter “\[Web messenger\] Website” - Select the Brand section. - Select the Channel section. 9. Click the **Save button to save the settings.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fv1su6tQMPg2EGABa537yumsgchosesave.png) **Try it:** 1. Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click on **Messenger** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FQvEGd5KwVrRzKs0tKyezOmsgbigchose.png) 4. Click on “ **<> ” from the Actions** menu next to **the Messenger name.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F8yiQI9g7Y7OW7dlxMxEmpmsgchoseintallcode.png) 5. Click on **Preview.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FV5DkZbXSfL7HqAiMO8GN5msgchosepreview.png) 6. In the window that appears, click on the circle in the lower right corner to try writing a chat. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F24hwWvahmuwE3RTBr2mHpmsgbuttonchoose.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FqhSRNT5YhmgCZjaXm2FDlmsgsentmsng.png) **If Web Messenger is not coming to TeamInbox, here are some things to check:** **1\. Check if the erxes account you are checking is included in the Channel:** - Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FZJK4b4Cp7Lx9i9e1dlqGsclickkkksettiongs.png) - Go to **Channel** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FEH2LvZBK5N5l5Ze4dYumHchosechsnnell.png) - **Check if your information is in the Member** section (erxes Account name) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F3cHSld98Oz3iSYgCoDKGNsettchanneledit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FcxhxD59kuSE81BpKv7hFWchannelchosemember.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhQWO8kWLlo6RnIrryRK5Osaveechannal.png) **2\. Check if Channel is selected for the Web messenger connection:** - Go to **Settings** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7ALJIWrz9J440Chn2WT6Echosesettings.png) - Go to **Integration** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) - Click on **Web Messenger** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FFe5abu75fZ72N1NHutOR1msgbigchose.png) - Click the **Edit** button and make sure **Channel is selected.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fpc11i4yAXTZmEeb8naB11msgchanneledit.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FYQPq5c3JaqtDhYFCG96lymsgchannelslide.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F6rPAzs4Fjr6WqC4fbaHxImsgchannelchose.png) --- # Connecting Instagram Post Published 2025-04-29 > Instructions to help you make the right Instagram Post connection. Connect Instagram Post Go to Settings . Navigate to Integration . Select Instagram Post and click +Add . Click Continue with Instagram and log in using t Instructions to help you make the right Instagram Post connection. ### **Connect Instagram Post** 1. **Go to Settings**. 2. Navigate to **Integration**. 3. Select **Instagram Post** and click **+Add**. 4. Click **Continue with Instagram** and log in using the **Admin** account of the Instagram business account you want to connect. > **Important:** 1. The account must have **Admin** rights to connect successfully. 2. Changing the Instagram password of the connected Admin account will disconnect the integration. 3. It is recommended to use a **non-personal, unused Admin account** to avoid any connection issues. 5. Click **Next**. 6. Click **Select** next to the Instagram business account name. 1. Ensure the business account is **Selected**. 7. In the **Integration Name** field, enter \[Insta post\] followed by the Instagram business account name. 8. Select the **Brand** section. 1. If you haven’t created one, click **Create Brand**, then select it. 9. Select the **Channel** section. 1. If you haven’t created one, click **Create Channel**, then select it. 10. Click **Save** to save the settings. ### **Test the Connection** 1. **Write a comment** under any post on the Instagram business account. 2. Go to the **erxes Team Inbox** window. 1. The comment should appear under the conversation section in the Team Inbox. 3. In the **To send your message** section, type a response to the comment. 4. Check if your response reaches the user’s Instagram post. ### **Troubleshooting (If Instagram Post comment is not Arriving in TeamInbox)** 1. #### 1\. **Check if the Team member is part of the Channel** 2. Go to **Settings → Channel**. 3. Check if your **erxes account** is listed under **Members**. 4. #### 2\. **Ensure Channel is selected in Instagram Post integration** 5. Go to **Settings → Integration**. 6. Click on **Instagram Post**. 7. Click **Edit** and make sure the **Channel** is selected. 8. #### 3\. **Recheck Instagram connection** 9. Go to **Settings → Integration**. 10. Click **+Add** under **Instagram Post**. 11. Click **Next**. 12. Click **Select** next to the Instagram business account name. 1. If **Select** is available, consider the connection missing and select the **Integration name**, **Brand**, and **Channel** again to save. 2. If you cannot select, assume the connection is successful and try other methods or contact **erxes Support** for assistance. 13. This process should ensure that Instagram Post integration is correctly set up and functioning in **Team Inbox**. **Illustrated instructions** : 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Select **Instagram Post** and click **+Add .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F0bHjYTcHwztcOMErIdZhmigpostadd.png) 4. **Click Continue with Instagram and log in as the "Admin" account** of the Instagram business account you want to connect . - **Note** : If the account is not an "Admin" account, the connection will not be successful, and the "Admin" account that has been connected cannot change its Instagram password. The connection will be disconnected every time it is changed. Therefore, Erxis recommends that you create an account that is not used by the "Admin" account and connect it. - If the "Admin" account's **"Admin" rights** are revoked, the connection will be lost. Only the "Admin" account will be able to reconnect properly, so it is necessary to select the correct "Admin" account. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FqFAHWrx5pJxHds4LLaezTigcontinuewithfb.png) 5. Click the **Next button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F1yXWuUaM1Q1BI0NlyYBn9igaccselect.png) 6. Click **Select** next to the Instagram business account name . - **It must be Selected** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FBNAQfxlc1lu0xFr0kgWGZigpostpageselect.png) 7. **In the Integration Name** field, enter **\[Insta post\] as the Instagram business account name .** 8. **Select the Brand** section. (If you haven't created one, you can create one by clicking **Create Brand and selecting it).** 9. **Select the Channel** section. (If you haven't created one, you can create one by clicking **Create Channel and selecting it).** 10. **Click the Save** button to save the settings. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FlmNkEj72l7qANFPnZqOZRigpostsave.png) **Test and check:** 1. Write a comment under the post of the Instagram business account. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FIXt8K8j3TgPwWx3BlBOqligpostig.png) 2. Go to the **erxes Team Inbox window.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzzRdNMPrzvNFWwOFs4hJQispostinbox.png) 3. The post you wrote will be successful if it enters this section. 4. In the "To send your message" section, type your response to the conversation and check if it reaches the user's post on the other side. **If Instagram Post is not arriving in TeamInbox, here are some things to check:** **Check if the erxes account you are checking is included in the channel:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. **Go to Channel** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FEH2LvZBK5N5l5Ze4dYumHchosechsnnell.png) 3. **Check if your information is in the Member** section (erxes Account name). ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F3cHSld98Oz3iSYgCoDKGNsettchanneledit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FcxhxD59kuSE81BpKv7hFWchannelchosemember.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhQWO8kWLlo6RnIrryRK5Osaveechannal.png) **Check if Channel is selected in Instagram connection:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7ALJIWrz9J440Chn2WT6Echosesettings.png) 2. **Go to Integration** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click on **Instagram Post .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FO3hCRXJQ5EYWZzlTQxOtPigpostbigchose.png) 4. Click the **Edit button and make sure Channel is selected.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FU9vdTvffZpMhJ22MeesfJigpostcheckedit.png) **Recheck your Instagram connection:** 1. **Go to Settings** . 2. **Go to Integration** . 3. **Click +Add in the Instagram Post** section . 4. Click the **Next button.** 5. Click **Select** next to the Instagram business account name . - **If Select is available** , consider the connection missing and select **Integration name** , **Brand** , and **Channel** again to save. - **If you are unable to select** , please assume the connection was successful and try other methods again, or contact our support team for assistance. --- # Connecting Instagram Messenger Published 2025-04-29 > Follow these steps to connect Instagram Messenger to erxes successfully: Go to Settings . Navigate to Integration . Select Instagram Messenger and click +Add . Click Continue with Instagram and log in using the Admin acc Follow these steps to connect Instagram Messenger to erxes successfully: 1. **Go to Settings**. 2. Navigate to **Integration**. 3. Select **Instagram Messenger** and click **+Add**. 4. Click **Continue with Instagram** and log in using the **Admin** account of the Instagram business account you want to connect. > **Important:** - The account must have **Admin** rights to successfully connect. - Changing the password of the connected Admin account will break the connection. - It is recommended to use a **non-personal, unused Admin account** to avoid any connection issues. 5. Click **Next**. 6. Click **Select** next to your Instagram business account name. Ensure that it is **Selected**. 7. In the **Integration Name** field, enter: \[Insta messenger\] Instagram Business Account Name. 8. Select the **Brand** section. - If not available, click **Create Brand**, then select it. 9. Select the **Channel** section. - If not available, click **Create Channel**, then select it. 10. Click **Save** to save the settings. ### **Test the Integration** 1. Open the **Instagram business account's messenger** and send a chat. 2. Go to **Team Inbox** in erxes. 3. Check if the message appears in the conversation section. 4. In the **To send your message** section, type your response and check if it is successfully sent to the customer on Instagram. ### **Troubleshooting (If Instagram Message is not Arriving in TeamInbox)** #### 1\. **Check if the Team member is part of the Channel** - Go to **Settings → Channel** - Ensure your **erxes account** is listed under **Members**. #### 2\. **Check if Channel is selected in Instagram Messenger integration** - Go to **Settings → Integration** - Click on **Instagram Messenger** - Click **Edit** and make sure the **Channel** is selected. #### 3\. **Recheck the Instagram connection** - Go to **Settings → Integration** - Click **+Add** under **Instagram Messenger** - Click **Next** - Click **Select** next to the Instagram business account name. - If **Select** is available, consider the connection missing and select the **Integration name**, **Brand**, and **Channel** again to save. - If you cannot select, assume the connection is successful and try other methods or contact **erxes Support** for help. This should help you ensure that Instagram Messenger is correctly connected and functioning in your **Team Inbox**. **Illustrated instructions** : 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Select **Instagram Messenger** and click **+Add .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7oJMpC07i2qyQ439vX4hqigmsgadd.png) 4. **Click Continue with Instagram and log in as the "Admin" account** of the Instagram business account you want to connect . - **Note** : If the account is not an "Admin" account, the connection will not be successful, and the "Admin" account that has been connected cannot change its Instagram password. The connection will be disconnected every time it is changed. Therefore, Erxis recommends that you create an account that is not used by the "Admin" account and connect it. - If the "Admin" account's **"Admin" rights** are revoked, the connection will be lost. Only the "Admin" account will be able to reconnect properly, so it is necessary to select the correct "Admin" account. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FqFAHWrx5pJxHds4LLaezTigcontinuewithfb.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FwYsWWd30UZmYnTkIcW1Dxcontituefbacc.png) 5. Click the **Next button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F1yXWuUaM1Q1BI0NlyYBn9igaccselect.png) 6. Click **Select** next to the Instagram business account name . - **It must be Selected** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FBNAQfxlc1lu0xFr0kgWGZigpostpageselect.png) 7. **In the Integration Name** field, enter **\[Insta messenger\] as the Instagram business account name .** 8. **Select the Brand** section. (If you haven't created one, you can create one by clicking **Create Brand and selecting it).** 9. **Select the Channel** section. (If you haven't created one, you can create one by clicking **Create Channel and selecting it).** 10. **Click the Save** button to save the settings. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FOKPWR2Qev3tkoei8HQ6n6igmsgsave.png) **Test and check:** 1. Chat on the Instagram business account's messenger. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FqmQ65S8d5s4rJ7EYhSQJ5igmsgig.png) 2. Go to the **erxes Team Inbox window.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FkKpB5VTHs1m9T6VyBIZZrigmsginbox.png) 3. The post you wrote will be successful if it enters this section. 4. In the "To send your message" section, type your response to the conversation and check if it reaches the user's post on the other side. **If Instagram Messenger is not coming to TeamInbox, here are some things to check:** **Check if the erxes account you are checking is included in the channel:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. **Go to Channel** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FEH2LvZBK5N5l5Ze4dYumHchosechsnnell.png) 3. **Check if your information is in the Member** section (erxes Account name). ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F3cHSld98Oz3iSYgCoDKGNsettchanneledit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FcxhxD59kuSE81BpKv7hFWchannelchosemember.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhQWO8kWLlo6RnIrryRK5Osaveechannal.png) **Check if Channel is selected in Instagram connection:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7ALJIWrz9J440Chn2WT6Echosesettings.png) 2. **Go to Integration** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click on **Instagram Messenger .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fj4TnZhyiT9viK02XLJ9H8igmsgbigchose.png) 4. Click the **Edit button and make sure Channel is selected.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F78xGLLqMr094AkJUUdHzTigcheckedit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FSxnva6itKyfyzOyA05zHEigcheckchannel.png) **Recheck your Instagram connection:** 1. **Go to Settings** . 2. **Go to Integration** . 3. **Click +Add in the Instagram Messenger** section . 4. Click the **Next button.** 5. Click **Select** next to the Instagram business account name . - **If Select is available** , consider the connection missing and select **Integration name** , **Brand** , and **Channel** again to save. - **If you are unable to select** , please assume the connection was successful and try other methods again, or contact our support team for assistance. --- # Connecting to Facebook Post Published 2025-04-29 > This guide will help you connect your Facebook Page's post comments to erxes correctly and troubleshoot if needed. Connect Facebook Post Go to Settings . Navigate to Integration . Select Facebook Post and click +Add . Cl This guide will help you connect your Facebook Page's post comments to erxes correctly and troubleshoot if needed. ### **Connect Facebook Post** 1. Go to **Settings**. 2. Navigate to **Integration**. 3. Select **Facebook Post** and click **+Add**. 4. Click **Continue with Facebook** and log in using the **Admin** account of the Facebook Page you want to connect. > **Important:** - The account must have **Admin** rights. - If not, the connection will **fail**. - Once connected, if the Admin account’s **password is changed**, the connection will **break**. - We recommend using a **dedicated Admin account** (non-personal and not in use for other purposes). - If Admin rights are revoked, the connection will be **lost**, and only the Admin can reconnect. 5. Click **Next**. 6. Click **Select** next to your Facebook Page name — it must be **Selected**. 7. In the **Integration Name** field, enter: \[Fb post\] Facebook Page Name 8. Select a **Brand**. - If not available, click **Create Brand**, then select it. 9. Select a **Channel**. - If not available, click **Create Channel**, then select it. 10. Click **Save**. ### **Test the Integration** 1. Go to your Facebook Page and **comment on any post**. 2. Open the **Team Inbox** in erxes. 3. Check if the comment appears in the conversation list. 4. In the **To send your message** section, write a reply. 5. Confirm that the reply is shown as a response to the customer’s post on Facebook. ### **If Facebook Post Comments Are Not Appearing in Team Inbox** #### **1\. Check if the Team member is part of the Channel** - Go to **Settings → Channel** - Open the relevant Channel - Ensure your **erxes account** is listed in the **Members** section #### **2\. Check if the Channel is properly selected in Facebook Post integration** - Go to **Settings → Integration** - Find **Facebook Post** - Click **Edit** - Confirm a **Channel** is selected #### **3\. Recheck the Facebook connection** - Go to **Settings → Integration** - Click **+Add** under **Facebook Post** - Click **Next** - If **Select** appears next to the Page, it means the connection is broken - Select the Page - Re-enter Integration name, Brand, and Channel - Click **Save** If the page cannot be selected, assume the connection is still valid. Retry the test or contact the **erxes Support Team** for assistance. **Illustrated instructions** : 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Select **Facebook Post** and click **+Add .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fed25uJEGSq1I0WIjxgBwHfbpostadddd.png) 4. **Click Continue with Facebook and log in with the "Admin" account** of the Facebook page you want to connect to . - **Note** : If the account is not an "Admin" account, the connection will not be successful, and the "Admin" account that has been connected cannot change its Facebook password. The connection will be disconnected every time you change it. Therefore, Erxis recommends that you create a non-personal, unused account for the "Admin" account and connect to it. - If the "Admin" account's **"Admin" rights** are revoked, the connection will be lost. Only the "Admin" account will be able to reconnect properly, so it is necessary to select the correct "Admin" account. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FX1uP3G5R1NsOXcvsvP6uicontinuewithfb.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FwYsWWd30UZmYnTkIcW1Dxcontituefbacc.png) 5. Click the **Next button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FAJhkVsjSjSGxyywZoPkwXchosefbaccnext.png) 6. Click **Select** next to the Facebook page name . - **It must be Selected** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FLKjMivjRhVBX3vQqQB9Dwchosefbpage.png) 7. **In the Integration Name** field, enter **\[Fb post\] as the Facebook page name .** 8. **Select the Brand** section. (If you haven't created one, you can create one by clicking **Create Brand and selecting it).** 9. **Select the Channel** section. (If you haven't created one, you can create one by clicking **Create Channel and selecting it).** 10. **Click the Save** button to save the settings. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzFQk7eiMH6sPs2bk2Xo8xfbpostsaveee.png) **Test and check:** 1. Write a comment under the post on the Facebook page. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FBjqeWYSlmEGr2zd6jWwdqfbpostcomme.png) 2. Go to the **erxes Team Inbox window.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FIVT0e5LaFDQ9y9yO9F2Wyinboxfbpost.png) 3. The post you wrote will be successful if it enters this section. 4. In the " **To send your message** " section, type your response to the conversation and check if it reaches the user's post on the other side. **If a Facebook Post is not arriving in TeamInbox, here are some things to check:** **Check if the erxes account you are checking is included in the channel:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FZJK4b4Cp7Lx9i9e1dlqGsclickkkksettiongs.png) 2. **Go to Channel** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FEH2LvZBK5N5l5Ze4dYumHchosechsnnell.png) 3. **Check if your information is in the Member** section (erxes Account name). ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F3cHSld98Oz3iSYgCoDKGNsettchanneledit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FcxhxD59kuSE81BpKv7hFWchannelchosemember.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhQWO8kWLlo6RnIrryRK5Osaveechannal.png) **Check if Channel is selected in Facebook connection:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7ALJIWrz9J440Chn2WT6Echosesettings.png) 2. **Go to Integration** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click on **Facebook Post .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FtKmQiX60YprvnjiPtqQPichosefbpostt.png) 4. Click the **Edit button and make sure Channel is selected.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FPIXBZoEMdT2BNVD75v1q8fbpostediiit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fu2xv9PRNJr4auEy4NtWSIcheckchannelsave.png)   **Check your Facebook connection again:** 1. **Go to Settings** . 2. **Go to Integration** . 3. **Click +Add in the Facebook Post** section . 4. Click the **Next button.** 5. Click **Select** next to the Facebook page name . - **If Select is available** , consider the connection missing and select **Integration name** , **Brand** , and **Channel** again to save. **If you are unable to select** , please assume the connection was successful and try other methods again, or contact our support team for assistance. --- # Connecting to Facebook Messenger Published 2025-04-29 > This guide explains how to correctly connect your Facebook Page Messenger to erxes and test whether it's working properly. Connect Facebook Messenger Go to Settings . Navigate to Integration . Select Facebook Messenger a This guide explains how to correctly connect your Facebook Page Messenger to erxes and test whether it's working properly. ### **Connect Facebook Messenger** 1. Go to **Settings**. 2. Navigate to **Integration**. 3. Select **Facebook Messenger** and click **+Add**. 4. Click **Continue with Facebook** and log in using the **Admin** account of the Facebook Page you want to connect. > **Important:** - You must log in with a **Facebook Page Admin account**. - If you are not an Admin, the connection will fail. - If the Admin account’s password is changed, the connection will be lost. - erxes recommends using a dedicated, non-personal Facebook account as the Admin for long-term stability. - If Admin rights are revoked, the connection will break. Only the Admin can restore it. 5. Click **Next**. 6. Click **Select** next to your Facebook Page name — it must be **Selected**. 7. In the **Integration Name** field, enter: \[Fb messenger\] Facebook Page Name 8. Select a **Brand**. - If not created, click **Create Brand**, then select it. 9. Select a **Channel**. - If not created, click **Create Channel**, then select it. 10. Click **Save**. ### **Test the Integration** 1. Send a message to your Facebook Page via Messenger. 2. Open the **Team Inbox** in erxes. 3. Confirm that the message appears in the conversation list. 4. In the **To send your message** field, enter your response and send. 5. Check if the response was delivered to the Facebook Messenger customer. ### **If Facebook Messages Are Not Appearing in Team Inbox** #### 1\. Check if your account is included in the Channel - Go to **Settings → Channel** - Select the Channel linked to Facebook - Check if your **erxes account** is listed under **Members** #### 2\. Verify the Channel is selected during Facebook Integration - Go to **Settings → Integration** - Find **Facebook Messenger** - Click **Edit** - Ensure a **Channel** is selected #### 3\. Recheck the Facebook connection - Go to **Settings → Integration** - Click **+Add** under Facebook Messenger - Click **Next** - If **Select** is available next to the Page, the connection may be lost. - Re-select the Page, enter Integration name, Brand, and Channel again - Click **Save** If you cannot select the Page, assume the connection is still valid. Retry or contact the **erxes Support Team** for help. **Illustrated instructions** : 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Go to **Integration .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Select **Facebook Messenger** and click **+Add .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F8UYoBKS36gB06jrAyMVbffbmsgaddd.png) 4. **Click Continue with Facebook and log in with the "Admin" account** of the Facebook page you want to connect to . - **Note** : If the account is not an "Admin" account, the connection will not be successful, and the "Admin" account that has been connected cannot change its Facebook password. The connection will be disconnected every time you change it. Therefore, Erxis recommends that you create a non-personal, unused account for the "Admin" account and connect to it. - If the "Admin" account's **"Admin" rights** are revoked, the connection will be lost. Only the "Admin" account will be able to reconnect properly, so it is necessary to select the correct "Admin" account. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FX1uP3G5R1NsOXcvsvP6uicontinuewithfb.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FwYsWWd30UZmYnTkIcW1Dxcontituefbacc.png) 5. Click the **Next button.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FAJhkVsjSjSGxyywZoPkwXchosefbaccnext.png) 6. Click **Select** next to the Facebook page name . - **It must be Selected** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FLKjMivjRhVBX3vQqQB9Dwchosefbpage.png) 7. **In the Integration Name** field, enter **\[Fb messenger\] as the Facebook page name .** 8. **Select the Brand** section. _(If you haven't created one, you can create one by clicking **Create Brand and selecting it).**_ 9. **Select the Channel** section. _(If you haven't created one, you can create one by clicking **Create Channel and selecting it).**_ 10. **Click the Save** button to save the settings. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Ff5d4mqyZdajJoD2ngZAAzfbmsgsavee.png) **Test and check:** 1. Chat on the Facebook page's messenger. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FNI6bhlnlhBJNnlAHFm1Dcsentmsgfbpage.png) 2. Go to the **erxes Team Inbox window.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fr1b5aDe02XY1MNmyNcE7lconv3rsation.png) 3. The message will be successful when it enters the message section. 4. In the " **To send your message** " section, type your response to the conversation and check if it arrives in the messenger of the user on the other side. **If Facebook page messages are not coming to TeamInbox, here are some things to check:** **Check if the erxes account you are checking is included in the channel:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FZJK4b4Cp7Lx9i9e1dlqGsclickkkksettiongs.png) 2. **Go to Channel** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FEH2LvZBK5N5l5Ze4dYumHchosechsnnell.png) 3. **Check if your information is in the Member** section (erxes Account name). ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F3cHSld98Oz3iSYgCoDKGNsettchanneledit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FcxhxD59kuSE81BpKv7hFWchannelchosemember.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FhQWO8kWLlo6RnIrryRK5Osaveechannal.png) **Check if Channel is selected in Facebook connection:** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F7ALJIWrz9J440Chn2WT6Echosesettings.png) 2. **Go to Integration** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FVNvfVKiTMFzxqWtGKs7jjchoseintegrationss.png) 3. Click on **Facebook Messenger .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FPg5SL9PmCqAzBFDj7dBp0chosefbmsg.png) 4. Click the **Edit button and make sure Channel is selected.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FvYoT2m0ebxjetcoJn9oeRchekkchanneledit.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fu2xv9PRNJr4auEy4NtWSIcheckchannelsave.png) **Check your Facebook connection again:** 1. **Go to Settings** . 2. **Go to Integration** . 3. **Click +Add in the Facebook Messenger** section . 4. Click the **Next button.** 5. Click **Select** next to the Facebook page name . - **If Select is available** , consider the connection missing and select **Integration name** , **Brand** , and **Channel** again to save. **If you are unable to select** , please assume the connection was successful and try other methods again, or contact our support team for assistance. --- # 7. Creating a 7-Day Customer Service Report and Custom Charts Published 2025-04-29 > This tutorial explains how to generate a weekly report specifically for the Customer Service Team and how to create a custom chart in the Insight module. Prepare a Report for the Last 7 Days Go to the Insight menu. Click This tutorial explains how to generate a weekly report specifically for the **Customer Service Team** and how to create a custom chart in the **Insight** module. ### **Prepare a Report for the Last 7 Days** 1. Go to the **Insight** menu. 2. Click **Add a Dashboard**. 3. In the **Dashboard name** field, enter: **"Last 7 Days Report"**. 4. In the **Choose Section** field, enter: **"Customer Service Team"**. 5. Click on the **Inbox** chart. 6. Check the following values: - **Total conversations count by source** - **Total conversations count** 7. Click **Save** to generate and save the report. ### **Create a New Chart** 1. Click **Add Chart**. 2. In the **Service** section, select: **Inbox**. 3. In the **Chart template** section, choose: **Total conversations count**. 4. In the **Select dimension** section, choose how you want to break down your data: - **Source** – Channels such as Facebook, Instagram, web form, call, etc. - **Tag** – Categorized labels. - **Frequency** – By time period (daily, weekly, etc.). - **Assign to** – Responsible **Team member**. - **Status** – Closed, Open, etc. 5. In the **Chart type** section, choose how your data should be displayed: - **Bar** – Column chart - **Pie** – Circular chart - **Line** – Line graph - **Table** – Tabular format (recommended for overview) 6. In the **Name** section, enter your desired chart name. > _Note: It’s best to enter the name last, as entering it first may result in the name being removed after choosing the chart template._ 7. In the **Select date range** section, choose your reporting time frame: - **Today** - **Yesterday** - **This Week** - **Last Week** - **This Month** - **Last Month** - **This Year** - **Last Year** 8. Click **Save** to finish creating the chart. **Illustrated instructions:** **Prepare a report for the last 7 days** 1. Go to the **Insight menu.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F0KZmsoF7IRAFwThZBeRhAchsoeinsight.png) 2. Click **Add a Dashboard .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FzphOzA9vaESD3mazH2k33adddashboard.png) 3. **In the Dashboard name** field, name it "Last 7 Days Report" 4. **In the Choose Section** section, name it "Customer Service Team". ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FvMfpbH0Ustqj8f81laLpRcreatesection.png) 5. Click on **the Inbox chart .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FeJNTkg70ETqUoXDUYlwUcchoseinboxchar.png) 6. Check the values ​​below: - **Total conversations count by source** - **Total conversations count** 7. **Click the Save** button to get the report. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FNu8BbQv8oa22F8xNLcmEflkajf.png)   **Create a new chart** 1. Click **Add Chart .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FZgQbaHrURU2OALNjTxMIjaddchartt.png) 2. **In the Service** section, select **Inbox .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FGp66RJ0h5mj5NiudB42zXchoseinboxx.png) 3. **In the Chart template** section , select **Total conversations count .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FjKutQMGIdfLlrhBqVfIYuchoseconvercount.png) 4. **In the Select dimension** section, enter the information you want to sort your data by: - **Source** : Through incoming channels (facebook, instagram, form, call, etc.) - **Tag** : Created by category - **Frequency** : Periodically - **Assign to** : Responsible employee - **Status** : Closed, Not Closed, etc. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FuN0Bq4apQMQepdZ4Y5Wrnchosefrequency.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FSYYi3k4ebmSMFJuPXJ1HZchosedatee.png) 5. **In the Chart type** section, select the type of chart you want to view: - **Bar** : By the column - **Pie** : In circular division - **Line** : Linear design - **Table** : By table (highest usage) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FujtFd87lD0eOl8FKYGFkRchosecharttype.png) 6. **In the Name** section, give it your desired name (Note: It is best to give it last, as it is deleted when you first give it a name after selecting the Chart template). 7. **In the Select date range** section, select the date range for the report. - **Today** : Today - **Yesterday** : Yesterday - **This week** : This week - **Last week** : Last week - **This Month** : This month - **Last Month** : Last month - **This Year** : This year - **Last Year** : Last year ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F4PSnCnhwDj9W6eesuOb9xchoselastweek.png) 8. Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FIQJJatAPgEIZWeJzRQ9WWsavesaveaa.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FWyxXLyQgqPueETw1Gcp4ilastinsightt.png) --- # 6. Instructions for Registering and Resolving Customer Feedback and Complaints Published 2025-04-29 > This guide helps simplify the process of documenting and managing feedback or complaints from Customers through a clear pipeline. Create a Feedback and Complaints Board Go to the Settings menu. Select Ticket Pipeline . C This guide helps simplify the process of documenting and managing feedback or complaints from **Customers** through a clear pipeline. ### **Create a Feedback and Complaints Board** 1. Go to the **Settings** menu. 2. Select **Ticket Pipeline**. 3. Click **Add New Board**, enter the name **“Feedback and Complaints”**, and click **Save**. ### **Set Up the Feedback Resolution Pipeline** 1. Click **Add New Pipeline**. _(This pipeline allows you to track and manage the stages of feedback and complaint resolution.)_ 2. In the **Name** field, enter: **“Feedback and Complaints”**. 3. Click **Add Another Stage**, enter: **“New”**. 4. Click **Add Another Stage**, enter: **“Distributed”**. 5. Click **Add Another Stage**, enter: **“Checking”**. 6. Click **Add Another Stage**, enter: **“Resolved”**. > Note: You can add more stages if necessary, but having too many stages may make the process more complicated. ### **Register Feedback and Complaints from a Conversation** 1. Go to the **Teaminbox** and open the relevant conversation. 2. Click the **Convert** button in the upper right corner. 3. Choose **Convert to a ticket**. 4. In the **Name** field, briefly describe the content of the complaint (e.g., “Late Delivery” or “Product Quality Concern”). 5. Click **Save** to register the complaint. **Illustrated instructions:** **Create a field for registering feedback and complaints** 1. **Go to Settings** . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Select **Ticket Pipeline .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FawZNDB3YsKmno19zgBkaVchosetickett.png) 3. **Click Add New Board** , type "Feedback and Complaints," and click **Save .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F4LDAyyTB30e4Gis6SCOy6addnewbaord.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fgty29K0hsUYj47proHRcXticketnamesave.png) 4. Click **Add New Pipeline .** _(Note: A field that allows you to manage the stages and steps of resolving feedback and complaints)._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F0HxbKjBxvN5FzggAw17P3addpipelee.png) 5. **In the Name** field, type "Feedback and Complaints". ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FlM4wDXNdwIqCmHO2qOykTpipelineename.png) - Click **Add Another Stage** and type "New" in **the Stage Name field.** - Click **Add Another Stage** and type "Distributed" in **the Stage Name field.** - Click **Add Another Stage** and type "Checking" in **the Stage Name field.** - Click **Add Another Stage** and type "Resolved" in **the Stage Name field.** _( **Note:** You can create as many stages as you want, but it's not a good idea to have too many.)_ 6. Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FgBCNMh7ewZJKax3fFKId3pipelineesave.png) **Registering feedback and complaints** 1. **Go to the Teaminbox** conversation. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FwY5vutPEim21Fvfg827WVchoseinbox.png) 2. Click the **Convert** button in the upper right corner . ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fc1xrhgq5LBM75zM4xhglOconverttickke.png) 3. Click **Convert to a ticket.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FNJv2z3V5CdBc93yCHB2Ilconverttoa.png) 4. **In the Name** field, enter the general content of the complaint. 5. Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FKQ6ddOC86VnaAOVz9kv9qsaveeticket.png) ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FHaG1lcTd8BmZ8DO2iQ4P7relatedtickcc.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fzy55dtCtJEfAVb869dyTNchosetiickck.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2Fs1q0jj9ssgVbhCFrXtmwgclickcarddaticket.png)![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FWwerdF2bEzOz8zQyRTn6pticketnameee.png) --- # 4. Setting Up Communication Channel Integrations Published 2025-04-29 > This section explains how to connect all available communication channels in the erxes application, helping you choose and integrate the right channel for your business needs. Instructions: Go to the official erxes guide This section explains how to connect **all available communication channels** in the erxes application, helping you choose and integrate the right channel for your business needs. #### **Instructions:** 1. Go to the official erxes guide page: [**erxes.io - Community - Guide - Frontline - Integration**](https://erxes.io/guides/6809e8a048a294faa11ea44e) 2. From the **"Integration"** category, you'll find connection guides for all major channels supported by erxes. 3. **Choose the channel** you wish to connect (such as Facebook, Instagram, Email, Viber, Webchat, etc.). 4. Follow the specific instructions provided for that channel to successfully integrate it into your erxes workspace. > 💡 This centralized guide allows any user to quickly set up external communication platforms with erxes for streamlined customer conversations. ![](https://imagedelivery.net/5m26Aj-CutMXPPNacMs_yQ/3c045f20-058f-45da-0aaa-596a654c8b00/public) --- # 3. Create a Channels and Brands Published 2025-04-29 > The Channel and Brand features in erxes help teams manage customer conversations more efficiently. Channels let you route inquiries to the right teams, while Brands allow you to organize customer data under different bra The **Channel** and **Brand** features in erxes help teams manage customer conversations more efficiently. Channels let you route inquiries to the right teams, while Brands allow you to organize customer data under different brand identities. #### **Step 1: Create a Channel** Channels help categorize customer messages by **team**, ensuring only the relevant team members can view and respond. 1. Go to the **Settings** menu. 2. Click on **Channels**. 3. Click the **Add New Channel** button. 4. In the **Name** field, enter the team name (e.g., "Customer Relations Team"). 5. In the **Members** section, add the employees who belong to this team. 6. Click **Save** to create the channel. > ✅ Channels improve service quality by assigning the right inquiries to the right team members. #### **Step 2: Create a Brand** Brands help segment customer communications and records under different brand identities within the same organization. 1. Go to the **Settings** menu. 2. Click on **Brands**. 3. Click the **Add New Brand** button. 4. In the **Name** field, enter the brand name (e.g., "CocaCola"). 5. Click **Save** to register the brand. > 💡 Use this feature if your organization manages multiple brands and needs to separate customer records and communication channels accordingly. **Illustrated instructions** **Create a Channel:** Categorize and distribute customer messages by team, making them visible only to relevant members. 1. Go to the **Settings menu.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png) 2. Click on **Channels .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F9wNGNlaaEWnvUZwBJhnQ0cahjnel.png) 3. **Click the Add New Channel** button and enter a team name (for example: “Customer Relations Team”). ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F5qvnWDHJTFsUguoeqwtWYaddnewchannelsum.png) 4. **In the Members** section, select and add employees belonging to the team. 5. Click **Save .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FT247Ebv8vCalznDsQ4R2jsavechannelsum.png)   **Brand creation:** If an organization has more than one brand, it will be used to record customer relationships separately for each brand. 1. Go to the **Settings menu.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FK35MHzsqlhrPTlSFIUJOTsetainsa.png) 2. Click on **Brands .** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FdjQpacYrm3WQFFLp0Pe73brandsaaa.png) 3. **Click the Add New Brand** button and enter a brand name (e.g., “CocaCola”) or a well-known brand name. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2F5OUUQKUkvhgeAQjHAM1xHaddnewbrandsum.png) 4. Click the **Save button to save.** ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io%2FBtkxHGLrG7y7O3qfn1mMjsavebrandsum.png) --- # 2. Creating an Organization, Adding Teams and Team Members Published 2025-04-29 > This section allows you to build your organization’s structure by creating branches, departments, and team members. The erxes platform’s permission system aligns with this structure, helping you manage teams and roles ef This section allows you to build your organization’s structure by creating branches, departments, and team members. The erxes platform’s permission system aligns with this structure, helping you manage teams and roles effectively. #### **Step 1: Create a Branch** 1. Go to the **Settings** section. 2. Navigate to **Team Member**. 3. Next to the **Branch** section, click the **"+"** icon. 4. In the **Title** field, enter the branch name. 5. In the **Address** field, enter the branch location. 6. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ 7. Click **Save**. _Tip: Fill in other optional fields if necessary before saving._ #### **Step 2: Create a Team or Department** 1. Next to the **Department** section, click the **"+"** icon. 2. In the **Title** field, enter the department or team name. 3. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ 4. In the **Team Members** field, assign an admin (usually yourself). 5. Click **Save** to finalize the team setup. #### **Step 3: Invite Team Members (Employees)** 1. Click the **Invite Team Member** button. 2. Fill in the following required fields: - **Email** - **Password** - **Permission Level** - **Branch** - **Team/Department** 3. Click **Save** to send the invitation and create the user profile. #### **Reminders:** - Invited users can **log in with the initial password** provided. - Users can **change their password** after logging in. - To **reset a user’s password**, the admin can: 1. Click the **lock icon** next to their email. 2. Enter the new password twice. 3. Click **Save** to apply the change. **Step 1: Create a Branch** 1. Go to the **Settings** section. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png)   2. Navigate to **Team Member**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/17Vx81G5orYAqJZlDlxemteamm.png)   3. Next to the **Branch** section, click the **"+"** icon. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/MteJoUGs40lv1zDHTvKyhbranchn.png)   4. In the **Title** field, enter the branch name. 5. In the **Address** field, enter the branch location. 6. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/vKdOG43DoPCJrIC6sXQnJbranchs.png)   7. Click **Save**. _Tip: Fill in other optional fields if necessary before saving._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/8Tc9ADyTrs9TIug9LUvygbranchsave.png) #### **Step 2: Create a Team or Department** 1. Next to the **Department** section, click the **"+"** icon. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/0bdg2aALls1DC0s2wmsF4depnem.png)   2. In the **Title** field, enter the department or team name. 3. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ 4. In the **Team Members** field, assign an admin (usually yourself). 5. Click **Save** to finalize the team setup. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/1CWg5h5ltYOM4w2GHmBlldepsave.png)   #### **Step 3: Invite Team Members (Employees)** 1. Click the **Invite Team Member** button. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/a25c2NISxcwg3GagteqWBinvtie.png) Fill in the following required fields: 1. **Email** 2. **Password** 3. **Permission Level** 4. **Branch** 5. **Team/Department** 2. Click **Save** to send the invitation and create the user profile. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/mRipmcoEw8KNx6MwcHTRMinviteee.png) #### **Reminders:** - Invited users can **log in with the initial password** provided. - Users can **change their password** after logging in. - To **reset a user’s password**, the admin can: 1. Click the **lock icon** next to their email. 2. Enter the new password twice. 3. Click **Save** to apply the change. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/UgM24KHtNqYyRL03eZGqIpassa.png) - ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/Hf510qMShl8O0VAKoebqUpassow.png) --- # 2. Creating an Organization, Adding Teams and Team Members Published 2025-04-29 > This section allows you to build your organization’s structure by creating branches, departments, and team members. The erxes platform’s permission system aligns with this structure, helping you manage teams and roles ef This section allows you to build your organization’s structure by creating branches, departments, and team members. The erxes platform’s permission system aligns with this structure, helping you manage teams and roles effectively. #### **Step 1: Create a Branch** 1. Go to the **Settings** section. 2. Navigate to **Team Member**. 3. Next to the **Branch** section, click the **"+"** icon. 4. In the **Title** field, enter the branch name. 5. In the **Address** field, enter the branch location. 6. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ 7. Click **Save**. _Tip: Fill in other optional fields if necessary before saving._ #### **Step 2: Create a Team or Department** 1. Next to the **Department** section, click the **"+"** icon. 2. In the **Title** field, enter the department or team name. 3. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ 4. In the **Team Members** field, assign an admin (usually yourself). 5. Click **Save** to finalize the team setup. #### **Step 3: Invite Team Members (Employees)** 1. Click the **Invite Team Member** button. 2. Fill in the following required fields: - **Email** - **Password** - **Permission Level** - **Branch** - **Team/Department** 3. Click **Save** to send the invitation and create the user profile. #### **Reminders:** - Invited users can **log in with the initial password** provided. - Users can **change their password** after logging in. - To **reset a user’s password**, the admin can: 1. Click the **lock icon** next to their email. 2. Enter the new password twice. 3. Click **Save** to apply the change. **Step 1: Create a Branch** 1. Go to the **Settings** section. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/RXkZYanFUkV9DaqiV3OeJerxesund.png)   2. Navigate to **Team Member**. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/17Vx81G5orYAqJZlDlxemteamm.png)   3. Next to the **Branch** section, click the **"+"** icon. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/MteJoUGs40lv1zDHTvKyhbranchn.png)   4. In the **Title** field, enter the branch name. 5. In the **Address** field, enter the branch location. 6. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/vKdOG43DoPCJrIC6sXQnJbranchs.png)   7. Click **Save**. _Tip: Fill in other optional fields if necessary before saving._ ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/8Tc9ADyTrs9TIug9LUvygbranchsave.png) #### **Step 2: Create a Team or Department** 1. Next to the **Department** section, click the **"+"** icon. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/0bdg2aALls1DC0s2wmsF4depnem.png)   2. In the **Title** field, enter the department or team name. 3. In the **Code** field, enter a unique code (e.g., "01"). _Note: This is a required field._ 4. In the **Team Members** field, assign an admin (usually yourself). 5. Click **Save** to finalize the team setup. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/1CWg5h5ltYOM4w2GHmBlldepsave.png)   #### **Step 3: Invite Team Members (Employees)** 1. Click the **Invite Team Member** button. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/a25c2NISxcwg3GagteqWBinvtie.png) Fill in the following required fields: 1. **Email** 2. **Password** 3. **Permission Level** 4. **Branch** 5. **Team/Department** 2. Click **Save** to send the invitation and create the user profile. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/mRipmcoEw8KNx6MwcHTRMinviteee.png) #### **Reminders:** - Invited users can **log in with the initial password** provided. - Users can **change their password** after logging in. - To **reset a user’s password**, the admin can: 1. Click the **lock icon** next to their email. 2. Enter the new password twice. 3. Click **Save** to apply the change. ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/UgM24KHtNqYyRL03eZGqIpassa.png) - ![](https://office.erxes.io/gateway/read-file?key=office-erxes-io/Hf510qMShl8O0VAKoebqUpassow.png) --- # [Notifications] Receive by email Published 2025-04-25 > Hello, you can receive notifications from the erxes via email by following the instructions below. . Go to Settings Select Notification config. Click on the Notification bar and scroll down. Get notification by email Che Hello, you can receive notifications from the erxes via email by following the instructions below. .  - Go to **Settings** - **Select Notification** config. - **Click on the Notification** bar and scroll down. - **Get notification by email** Check and click save. --- # 1. Invite Team Members Published 2025-04-24 > 1. Go to Settings - Navigate to the Team Members section 2. Click the ➕ I nvite team members: Fill in the required fields in the form: - Email address - Enter the team member's email - Password - Create a temporary passw 1\. Go to **Settings**     - Navigate to the **Team Members** section 2\. Click the ➕ I**nvite team members:** Fill in the required fields in the form:     - **Email address** - Enter the team member's email      **- Password** - Create a temporary password      **- Permission** - Select the appropriate Permission group      **- Channels** - Choose the relevant channel      **- Unit, Department, Branch** - Assign the team member to their respective unit,         department, and branch 4\. Click **Save** _Note: The team member will receive an email to set up their account by clicking the link, entering their name, and creating a new password_ --- # 1. Signing up Published 2024-12-02 > Introduction - To successfully sign up for Erxes and start using it right away, you need to follow these two simple steps: 1. Create your Global Profile. 2. Create your organization from your Global Profile. Note: Your G **Introduction** \- To successfully sign up for Erxes and start using it right away, you need to follow these two simple steps:    1. Create your Global Profile.    2. Create your organization from your Global Profile. _Note: Your Global Profile allows you to manage multiple organizations. Select your desired organization from your Global Profile to proceed to step two._ **Step-by-Step Instructions** \- Create Your Global Profile:    1. Go to [**https://erxes.io/signup**.](https://erxes.io/signup) -    2. Enter your email address and password (_or use the magic link option to sign up_). -        Note: This email address will be registered as the owner of your Global Profile. -    3. Enter the verification code sent to your email. -    4. Log in with your email and password to complete your Global Profile setup. ![Click on Email](https://images.tango.us/workflows/9c2fe615-6b0f-4014-a7d3-3f840da04e4b/steps/284e8c13-a3ef-4c62-8e26-da0425a60ec2/b9f90a82-b8ff-4b5c-8f42-883d79e82586.png?mark-x=1184&mark-y=724&m64=aHR0cHM6Ly9pbWFnZXMudGFuZ28udXMvc3RhdGljL2JsYW5rLnBuZz9tYXNrPWNvcm5lcnMmYm9yZGVyPTQlMkM1NjI5QjYmdz04MTYmaD05NiZmaXQ9Y3JvcCZjb3JuZXItcmFkaXVzPTEw) - \- Create Your Organization -    1. Click on “Create a new organization.” ![](https://office.erxes.io/gateway/read-file?key=0.41913344547008213image-20230515002129-8.png) - _Enter the name of your organization_ - _Enter your team size_ - _Specify your industry and your revenue_ - _Click on “Create”_ - _Select your created organization in order to access the log-in page for that specific organization_ ![](https://office.erxes.io/gateway/read-file?key=0.5971041007011282image-20230515003754-9.png) ![](https://office.erxes.io/gateway/read-file?key=0.04356846022975702image-20230515003836-10.png) ![](https://office.erxes.io/gateway/read-file?key=0.6204186433359826image-20230515004035-14.png) - _Log in to your organization by entering the same email address and password used for your Global Profile. **Note down your subdomain.app.erxes.io URL that is displayed on the top to access your organization directly in the future without the need to go to your Global Profile unless you want to create a new organization or buy add-ons.**_ ![](https://office.erxes.io/gateway/read-file?key=0.7159259869867662image-20230515004009-13.png) - _Enter your full name and desired username after clicking “Next”_ - _Follow the rest of the guide located on the bottom left corner to set up your erxes!_ --- # What is erxes Milestones? Published 2024-09-05 > We are currently piloting this rewards program to offer our customers even more reason to enjoy erxes. By joining this program, you could receive up to $80 in credit, receive our exclusive swag, or even become an erxes a We are currently piloting this rewards program to offer our customers even more reason to enjoy erxes. By joining this program, you could receive up to $80 in credit, receive our exclusive swag, or even become an erxes ambassador! It is absolutely free to join the program. There are 5 stages of milestones. You can learn more about the individual levels from the [FAQ](https://erxes.io/guides) or the [dedicated page](https://erxes.io/team). --- # What do I need to do to unlock the second stage: Catalyst? Published 2024-09-05 > Invite other businesses and you will earn $20 credit for 4 successful referrals. You can choose to redeem it right then or wait until you collect all $40 credits. Just let us know which feature you'd like to upgrade by e Invite other businesses and you will earn $20 credit for 4 successful referrals. You can choose to redeem it right then or wait until you collect all $40 credits. Just let us know which feature you'd like to upgrade by emailing your preference via growth @ erxes.io. Once you successfully invite 8 other companies, you will unlock this stage and become an **erxes Catalyst**! --- # What is CSO? Published 2024-09-05 > A Continuous Securities Offering (CSO) is a new, updated way for companies to raise funding. A Continuous Securities Offering (CSO) is a new investment vehicle, updated for the digital era. The CSO enables companies with A Continuous Securities Offering (CSO) is a new, updated way for companies to raise funding. A Continuous Securities Offering (CSO) is a new investment vehicle, updated for the digital era. The CSO enables companies with growth potential to raise funding by selling a claim on a reserve, funded primarily by a fixed portion of revenues. It expands the pool of potential investors, allowing all stakeholders — not just a small group of privileged, wealthy investors — to share in the value created by a company’s success. --- # Who can invest? Published 2024-09-05 > This investment opportunity is open to everyone around the world, with the exception of US accredited investors. This investment opportunity is open to everyone around the world. But please note that if you’re a US citiz This investment opportunity is open to everyone around the world, with the exception of US accredited investors. This investment opportunity is open to everyone around the world. But please note that if you’re a US citizen or entity, you must be an accredited investor to comply with relevant regulations. --- # When does the offering start and end? Published 2024-09-05 > Nov 23, 2020 at 08:00 PM PST. The CSO launch date is Nov 23, 2020 at 08:00 PM PST. As for the closing date, contrary to other offerings, CSO runs continuously. Nov 23, 2020 at 08:00 PM PST. The CSO launch date is Nov 23, 2020 at 08:00 PM PST. As for the closing date, contrary to other offerings, CSO runs continuously. --- # When can I start selling or transferring my $ERXS? Published 2024-09-05 > After 12 months Your purchased $ERXS can only be sold or transferred after 12 months. After 12 months Your purchased $ERXS can only be sold or transferred after 12 months. --- # What does erxes mean? How do you pronounce it? Published 2024-09-05 > erxes (pronounced 'erk-sis') is a Mongolian word meaning “galaxy”. A galaxy is a system of stars, stellar remnants, interstellar gas, dust, and dark matter bound together by gravity. The word is derived from the Greek ga erxes (pronounced 'erk-sis') is a Mongolian word meaning “galaxy”. A galaxy is a system of stars, stellar remnants, interstellar gas, dust, and dark matter bound together by gravity. The word is derived from the Greek galaxias (γαλαξίας), literally 'milky', a reference to the Milky Way galaxy that contains the Solar System. It is branded as “erxes” with all lowercase letters. erxes - Improving everyday experiences can significantly enhance the quality of life. These small, incremental improvements can lead to substantial benefits and better living. Better living and happier people will have a contagious effect on everything they touch, eventually improving the universe. --- # 3. Create your Brands and Channels Published 2024-09-05 > How to create your brands and channels in erxes? You need to distinguish your brands for their respective integrations, automation, forms, etc For example, erxes have two different brands named erxes Inc and erxes Mongol ## **How to create your brands and channels in erxes?** You need to distinguish your brands for their respective integrations, automation, forms, etc For example, erxes have **two different brands** named erxes Inc and erxes Mongolia to distinguish between international and local customers to custom target them and absorb marketing data. Channels are a bit different in that you can completely disappear your already created brands for certain team members during the integration process. So without further ado, let's get straight into it! In order to create a brand you have to: - _Log in to your organization_ - _Navigate to the "Settings" page_ - _Click on "Brands"_ - _Select "+ Add New Brand"_ - _Give it a unique name. **Please note that you will see this name across the whole platform so it should be distinguishable from your brands.**_ - _Give it a description if you want to_ - _You can leave the email parts for now_ - _Click "Save" to save your brand_ - _Repeat the previous 5 steps for each of your brands_ **![](https://lh4.googleusercontent.com/a2Sfntl3D8Y_iXrSX2aI-jFSqiVA9pqbFkd7XYa__mHuANmHe6_E8QQmAj7UE4JiU6OC5a2byxpKSsCaJBkCyriNCHR8BOu0ENqEGeCNhajVyoG-cIf2G_mfK5_1BcY_dFYSn6Cn)** In order to create your channels you have to:  - _Log in to your organization_ - _Navigate to the "Settings" page_ - _Click on "Channels"_ - _Select "+ Add New Channel"_ - _Give your channel a name_ - _Give it a description if you want to_ - _Add the members you want to include in this channel_ - _Repeat the previous 4 steps for the rest of the channels_ **![](https://lh3.googleusercontent.com/AM3cuGB7rpib8G1TSQavNtga-yGuwwGJ6xBUrng-_OteCb_BoFB__hPzj246L64MSOd_nIq2vdLtMLRQeg4XVBacT4uVFGqAH72Iw1ZZmTID_S_P54xLoojDUA6GlW0qfwFMXbFs)**