# 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://<domain>/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:<serviceName>` proxies directly to the address that `<serviceName>` 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-<name>`: the service's HTTP address.
- `erxesservice:config:<name>`: 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:<port>` in development and `http://plugin-<name>-api:<port>` 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-<name>` exists (`retryGetProxyTarget`, one attempt per second) and until `POST <address>/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 `<address>/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:<name>`.

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:<id>` (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:<subdomain>:<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_<userId>_<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 |

