# 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 `<subdomain>`, 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-<name>-api:<port>` in production, `http://localhost:<port>` 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 `<db>_logs` from it |
| `DB_NAME` | `erxes-api-shared` (saas) | saas only | `erxes_<organizationId>` | 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` | `<proto>//<host>/gateway` (prod), `http://localhost:4000` (dev) | Public gateway URL the browser calls; `<subdomain>` 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`.

