# Environment Variable Reference

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `deploy/.env.example`, `deploy/generate-env.sh`, `crates/cheng-api/src/services/env_settings/registry.rs`, `config/decision_providers.toml`

Every ChengOS deployment mode — native, Docker, and distributed — reads the same
`deploy/.env` file. This page is the parameter dictionary. For how to actually deploy, see
[Install ChengOS](/docs/install-chengos) and [Docker deployment](/docs/deploy-docker).

Start from the template rather than an empty file:

```bash
cp deploy/.env.example deploy/.env
```

`chengos.sh install` and `generate-env.sh` fill in generated secrets and derive ports and
paths, so in most cases you edit only what you want to differ from the defaults.

`.env` is **instance-owned**: an upgrade never overwrites it. Defaults introduced by a newer
release — such as the loopback API binding in v0.2.0 — apply to new installs only, so compare
your file against the new `.env.example` after each upgrade.

## Required secrets

These have placeholder values in the template. An installation that leaves them unchanged
is not secure. Generate each one before exposing the system.

| Variable | Generate with | Notes |
|---|---|---|
| `CREDENTIAL_MASTER_KEY_1` | `openssl rand -hex 32` | Encrypts every stored credential. **Losing it makes stored credentials unrecoverable**; changing it strands the existing ones. |
| `JWT_SECRET` | `openssl rand -hex 32` | Signs API access tokens. Changing it invalidates every issued session. |
| `POSTGRES_PASSWORD` | `openssl rand -hex 16` | Must match the password inside `DATABASE_URL`. |
| `REDIS_PASSWORD` | `openssl rand -hex 16` | Must match the password inside `REDIS_URL`. |

## Datastore connections

```bash
DATABASE_URL=postgres://tianai_db:<password>@127.0.0.1:5432/master_router
REDIS_URL=redis://:<password>@127.0.0.1:6379
QDRANT_URL=http://127.0.0.1:6334
```

The hostname depends on the deployment mode: `127.0.0.1` for native and `managed-process`
installs, the Compose service names `postgres` / `redis` / `qdrant` for Docker, and your
own host for distributed installs.

Agent memory nodes read the connection as discrete values rather than a URL, so these must
stay consistent with `DATABASE_URL` and `REDIS_URL`:

| Variable | Default |
|---|---|
| `DB_HOST` / `DB_PORT` | `127.0.0.1` / `5432` |
| `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `master_router` / `tianai_db` / — |
| `REDIS_HOST` / `REDIS_PORT` / `REDIS_DB` | `127.0.0.1` / `6379` / `0` |

Pool tuning is available for both: `DATABASE_MAX_CONNECTIONS` (10),
`DATABASE_MIN_CONNECTIONS` (1), `DATABASE_CONNECTION_TIMEOUT` (30), and the matching
`REDIS_MAX_CONNECTIONS`, `REDIS_MIN_CONNECTIONS`, `REDIS_CONNECTION_TIMEOUT`.

## Ports and server binding

| Variable | Default | Service |
|---|---|---|
| `PORT` / `API_PORT` | `19225` | API backend and WebSocket |
| `UI_PORT` | `8080` | Visual editor and admin console |
| `APP_PORT` | `5055` | Chat app and mobile H5 |
| `BIND_ADDRESS` | `127.0.0.1` | API bind address |
| `UI_BIND` / `APP_BIND` | `0.0.0.0` | Interface the native UI / app servers listen on |

The API stays on loopback in every supported layout: the UI and app servers proxy `/api` and
`/ws` to it, and the CLI and health checks dial `127.0.0.1`. `0.0.0.0` would put the API on the
network beside the hardened frontends rather than behind them. A remote CLI or desktop client
should go through your reverse proxy on 443 instead.

`UI_BIND` / `APP_BIND` select an interface; they are not access control. Use `127.0.0.1` when
the proxy is on the same host, or a private/VPN address of this host when it is elsewhere.

Docker ignores all three: `docker-compose.yml` publishes host ports to `127.0.0.1` only and pins
`BIND_ADDRESS=0.0.0.0` *inside* the API container, because the UI container must reach it over
the Compose network.

## Edge and proxy trust

| Variable | Default | Notes |
|---|---|---|
| `TRUSTED_PROXY_IPS` | empty | Comma-separated addresses or CIDRs of proxies allowed to report a visitor's real IP. Loopback and RFC 1918 peers are trusted without being listed; a proxy on a public or CGNAT (`100.64.0.0/10`) address must be listed, or every visitor shares one rate-limit bucket. |
| `TRUST_CLOUDFLARE` | `false` | Also trust Cloudflare's ranges and read `CF-Connecting-IP`. Toggle with `./chengos.sh cloudflare on\|off`. |
| `CSP_CONNECT_SRC_EXTRA` | empty | Extra origins, space-separated, for the frontends' CSP `connect-src`. |

See [Hardening a Public Deployment](/docs/deploy-hardening) for how these fit together.

## Release pinning

| Variable | Default | Notes |
|---|---|---|
| `CHENGOS_VERSION` | `latest` | The single release pin for all four Docker images. Set an exact version in production. |
| `CHENGOS_<SERVICE>_IMAGE` | unset | Per-image override for private registries. Any override disables coordinated release updates. |
| `CHENGOS_IMAGE` | `chengos/chengos:latest` | Legacy alias for older Compose overlays. Prefer `CHENGOS_VERSION`. |

## Runtime behaviour

| Variable | Default | Notes |
|---|---|---|
| `RUN_MIGRATIONS` | `true` | Apply database migrations on startup. Ignored while demo mode is on. |
| `CHENG_DEMO_MODE` | `false` | Fail-closed public demo allowlist. Initialize the demo user *before* enabling. See [demo mode](/docs/demo-mode). |
| `CHENG_DEMO_USER_EMAIL` | unset | The ordinary, non-admin account demo visitors are signed in as. |
| `RUST_LOG` | `info` | Log level. |
| `RUST_BACKTRACE` | `1` | Include backtraces in error logs. |
| `DB_INSTALL_MODE` | `managed-process` | `managed-process` (user sandbox, no root) or `system-service` (apt + systemd, needs `sudo`). |
| `ENABLE_REDIS` | `false` | Turn on when using agent memory. |
| `ENABLE_QDRANT` | `false` | Turn on when using RAG retrieval. |

## Scheduler

Governs timed and triggered workflow runs.

| Variable | Default | Notes |
|---|---|---|
| `SCHEDULER_ENABLED` | `true` | Master switch. |
| `SCHEDULER_NODE_ID` | `node-1` | Must be unique per instance in a multi-instance deployment. |
| `SCHEDULER_MAX_CONCURRENCY` | `8` | Simultaneous scheduled executions. |
| `SCHEDULER_HOT_WINDOW_SECS` | `60` | Look-ahead for imminent jobs. |
| `SCHEDULER_WARM_WINDOW_SECS` | `300` | Look-ahead for upcoming jobs. |
| `SCHEDULER_SWEEPER_INTERVAL_SECS` | `30` | Sweep interval for due jobs. |
| `SCHEDULER_LOCK_TIMEOUT_SECS` | `60` | Lock lease before another instance may claim a job. |

## CORS

```bash
CORS_PERMISSIVE=false
CORS_ALLOWED_ORIGINS=
```

CORS is deny-by-default, because every supported deployment is same-origin: the UI and app
servers proxy `/api` and `/ws` themselves. Fill `CORS_ALLOWED_ORIGINS` (comma-separated) only
when a browser on a *different* origin must call the API. `CORS_PERMISSIVE=true` allows any
origin, logs a warning on every start, and is for local experiments only.

## Workspaces and sandboxes

| Variable | Notes |
|---|---|
| `CHENG_CLI_ALLOWED_ROOTS` | Colon-separated absolute directories usable as CLI sandboxes. **CLI sessions are disabled entirely if this is unset.** The CLI and API must see these paths in the same filesystem namespace. |
| `CHENG_WORKSPACE_ROOT` | Base directory for `code_shell` / `code_python` when no CLI sandbox is present. Defaults to `{repo_root}/workspaces` or `./workspaces`. |
| `SKILLS_DIR` | Skill library location. Default `./skills`. |
| `NODE_SKILLS_DIR` | Per-node skill definitions. Docker default `/app/node_skills`. |
| `CHENG_GLOBAL_CONFIG_DIR` | Shared `shortcuts.yaml` and `presets.yaml`. Docker `/app/config`; native auto-detects. Per-workspace configs override these. |
| `TEMPLATE_WORKFLOWS_DIR` | Read-only built-in workflow templates installed into new workspaces on bootstrap. Docker `/app/workflow-templates`; native auto-detects. |

Agent tool approval — see [Tool-call Approval Policy](/docs/agent-approval-policy):

| Variable | Notes |
|---|---|
| `CHENG_FILE_OPS_PROTECTED_PATTERNS` | Comma-separated gitignore-style patterns **added** to the built-in protected paths. It can only add. |
| `CHENG_TRUSTED_AUTOMATION_POLICY_FILE` | JSON file of scoped entries that let named workflows run deletes, moves, and rollbacks unattended. |
| `CHENG_WORKSPACE_PROCESS_APPROVAL_DISABLED` | `true` removes the approval gate on workspace shell and test runs for the whole deployment. Trusted automation servers only. |

## Decision models

Settings for the typed decision providers behind `ai/decision_branch`. They are read once at
startup. Details: [Decision Providers & TypeSafe Jev](/docs/decision-providers-and-jev).

| Variable | Default | Notes |
|---|---|---|
| `JEV_API_KEY` | unset | Deployment-managed TypeSafe Jev key, used by nodes that select no credential. Routed request text is sent to TypeSafe. |
| `JEV_DEFAULT_MODEL` | `jev-1.13.0` | Jev model when neither the node nor its credential pins one. Use an exact version with a calibration profile. |
| `CHENG_DECISION_PROVIDERS_TOML` | unset | Replacement for the shipped `decision_providers.toml`. An invalid file is ignored. |
| `CHENG_DECISION_CALIBRATION_TOML` | unset | Replacement for the shipped calibration profiles. An invalid file is ignored. |
| `LAYA_ENABLED` | `false` | Enable the local Laya decision model (`laya serve`). |
| `LAYA_PORT` | `1215` | Laya on the API host's loopback. Never 8080, which `UI_PORT` uses. |
| `LAYA_ENDPOINT` | unset | Full Laya address instead, e.g. `http://host.docker.internal:1215` from Docker. |
| `LAYA_TRUSTED_NETWORK` | `false` | Allow a non-loopback plain-http `LAYA_ENDPOINT`. |
| `LAYA_DEFAULT_MODEL` | `multilingual` | Laya family when a node names none. |
| `LAYA_API_KEY` | unset | Bearer token shared with `laya serve`. |
| `LAYA_ARTIFACT_SHA256`, `LAYA_ARTIFACT_SHA256_<FAMILY>` | unset | SHA-256 pin of the served GGUF; calibration applies only to a pinned file. |
| `LAYA_BIN`, `LAYA_MODEL_PATH`, `LAYA_MODELS_DIR`, `LAYA_DEVICE`, `LAYA_LD_LIBRARY_PATH` | unset | Native mode only: let `hybrid/start.sh` launch and verify `laya serve`. Leave empty to run it yourself. |

Jev endpoints and decision timeouts are TOML settings in `decision_providers.toml`, not
environment variables.

## Code execution and model endpoints

| Variable | Default | Notes |
|---|---|---|
| `CHENG_ENABLE_CODE_PYTHON` | `true` | Enables the `tools/code_python` node. |
| `CHENG_ENABLE_CODE_JS` | `true` | Enables the `tools/code_js` node. |
| `ALLOW_PRIVATE_LLM_ENDPOINTS` | `true` | Permits model endpoints on private network addresses, such as a local Ollama. Set `false` to block requests to private ranges. |

## Optional integrations

**MCP** — `MCP_ENABLED` (`false`) mounts the app-level MCP server at `/mcp`.
`MCP_SERVER_TOKEN` sets a static bearer token; leave it unset to use database-backed MCP
tokens, which is the preferred mode. `MCP_GATEWAY_PORT_START` (`9100`) is the base port for
per-workflow dynamic servers.

**OAuth2** — client IDs and secrets come from each provider's app console:
`OAUTH2_OPENAI_*`, `OAUTH2_ANTHROPIC_*`, `OAUTH2_GOOGLE_*`, `OAUTH2_GITHUB_*`,
`OAUTH2_MICROSOFT_*`. PKCE providers need no client secret; GitHub OAuth apps generally do.
`OAUTH2_REDIRECT_URI` must match the redirect URI registered with the provider exactly.

**Email** — `EMAIL_ENABLED` (`false`), then `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`,
`SMTP_PASSWORD`, `SMTP_FROM`, and `PASSWORD_RESET_BASE_URL`. Required for password reset
mail.

**Public URLs** — used by Docker deployments to tell the frontends where to reach the
backend: `UI_API_BASE_URL` (`/api/v1`), `UI_WS_URL` (`/ws`), `APP_API_BASE_URL`,
`APP_WS_URL` (`/ws/executions`), `APP_CHANNEL_ID`, `APP_BOUND_WORKFLOW_ID`, and the
externally visible `PUBLIC_UI_URL`, `PUBLIC_APP_URL`, `PUBLIC_API_URL`.

**CLI** — `CHENG_SERVER_URL` is the server the `cheng` client connects to. Docker
deployments default it to `http://api:3000`.

## Next steps

- [REST API overview](/docs/api-rest-overview) — what those endpoints and headers are.
- [Docker deployment](/docs/deploy-docker) — the mode-specific hostname rules.
- [Hardening a Public Deployment](/docs/deploy-hardening) — the binding and trust settings in context.
