# Environment Variables

> Complete reference for all environment variables used by the Menace Voice backend

Core environment variables are centralized in `api/constants.py`. Variables marked **Required** in the description must be explicitly set — the application will either fail to boot or behave insecurely without them.

## Deployment Modes

Menace Voice supports two deployment modes, set via `DEPLOYMENT_MODE`:

- **Self-hosted**: The default mode. Designed for deployments using [Docker Compose](/deployment/docker) — the fastest way to get Menace Voice running. Uses local JWT authentication and MinIO for storage.
- **SaaS**: Intended for customised deployments outside of Docker. Authentication and API key management are handled through Menace Voice Managed Platform Services (MPS), allowing greater flexibility in how the platform is hosted and integrated.

The relevant required variables for each mode are noted in the descriptions below.

---

## Application

| Variable | Default | Description |
|---|---|---|
| `ENVIRONMENT` | `local` | Runtime environment. Affects logging and behaviour. One of `local`, `production`, `test` |
| `DEPLOYMENT_MODE` | `oss` | Deployment mode. Use `oss` for self-hosted |
| `AUTH_PROVIDER` | `local` (or `stack` when Hexclave project id + secret are set) | Authentication provider. `local` uses the built-in email/password flow. Unset + Hexclave keys enables `stack`. Set `AUTH_PROVIDER=local` to force local. See [Authentication](/deployment/authentication) |

---

## Database

| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | N/A | **Required.** PostgreSQL connection string. e.g. `postgresql+asyncpg://user:pass@host:5432/dbname` |
| `REDIS_URL` | N/A | **Required.** Redis connection string. e.g. `redis://localhost:6379` |

---

## Authentication (Self-hosted)

| Variable | Default | Description |
|---|---|---|
| `OSS_JWT_SECRET` | N/A | **Required for self-hosted deployments.** Secret used to sign JWT tokens. Must be set to a strong random value in production |
| `OSS_JWT_EXPIRY_HOURS` | `720` | JWT token lifetime in hours (default: 30 days) |
| `ENABLE_SIGNUP` | `true` | Set to `false` to disable public signup on invite-only installs — `POST /api/v1/auth/signup` returns 403 and the login page hides the Sign up link |

<Warning>
Never use the placeholder `OSS_JWT_SECRET` in a production deployment. Generate a strong random secret and store it securely.
</Warning>

---

## Authentication (Stack Auth)

Set a Hexclave/Stack **project id and secret** in the compose-root `.env` to enable social login. `AUTH_PROVIDER=stack` is optional when those keys are present; set `AUTH_PROVIDER=local` to keep email/password. The project id and publishable client key are public and are served to the browser at runtime via `/api/v1/health`; the secret server key stays server-side. See [Authentication](/deployment/authentication) for the full walkthrough.

On a Docker install, put these in the **compose-root `.env`** (next to `docker-compose.yaml`), not `api/.env`. Compose only injects variables listed in each service's `environment:` block.

| Variable | Default | Description |
|---|---|---|
| `HEXCLAVE_PROJECT_ID` / `STACK_AUTH_PROJECT_ID` | `null` | **Required for `stack`.** Project ID (public) |
| `STACK_PUBLISHABLE_CLIENT_KEY` | `null` | Publishable client key (public). Optional for newer Hexclave projects |
| `HEXCLAVE_SECRET_SERVER_KEY` / `STACK_SECRET_SERVER_KEY` | `null` | **Required for `stack`.** Secret server key — server-side only, also set on the `ui` service. Keep secret |
| `HEXCLAVE_API_URL` / `STACK_AUTH_API_URL` | `https://api.hexclave.com` | REST API base URL |

---

## URLs

| Variable | Default | Description |
|---|---|---|
| `PUBLIC_BASE_URL` | `null` | Canonical public origin for the deployment (scheme + host, e.g. `https://203-0-113-10.sslip.io`). For a standard single-host install this is the only endpoint value you set — `BACKEND_API_ENDPOINT` and `MINIO_PUBLIC_ENDPOINT` derive from it |
| `PUBLIC_HOST` | `null` | Public host without scheme (e.g. `203-0-113-10.sslip.io`); `TURN_HOST` derives from it |
| `BACKEND_API_ENDPOINT` | `PUBLIC_BASE_URL`, else `http://localhost:8000` | Public URL the backend builds webhook / callback / embed links from. Set explicitly only to override the value derived from `PUBLIC_BASE_URL` |
| `UI_APP_URL` | `http://localhost:3010` | URL of the frontend application |
| `MPS_API_URL` | `https://services.dograh.com` | Menace Voice Managed Platform Services URL |
| `DOGRAH_MPS_SECRET_KEY` | `null` | **Required for managed deployments.** Secret key for authenticating with MPS |
| `CORS_ALLOWED_ORIGINS` | `null` | **Required for managed deployments.** Comma-separated list of origins allowed to make credentialed cross-origin requests (e.g. `https://app.example.com,https://admin.example.com`). Ignored in self-hosted mode, which serves a permissive same-origin policy without credentials |

---

## Storage

Menace Voice uses **MinIO by default**, which is bundled with the self-hosted deployment and requires no external setup. Set `ENABLE_AWS_S3=true` to switch to AWS S3 — typically used for cloud or managed deployments where S3 is already part of the infrastructure.

### MinIO (self-hosted default)

| Variable | Default | Description |
|---|---|---|
| `MINIO_ENDPOINT` | `localhost:9000` | MinIO server host and port |
| `MINIO_PUBLIC_ENDPOINT` | `PUBLIC_BASE_URL`, else `http://localhost:9000` | Publicly accessible MinIO URL for download links. Derives from `PUBLIC_BASE_URL`; set explicitly only for a separate object-storage origin |
| `MINIO_ACCESS_KEY` | N/A | **Required for self-hosted deployments.** MinIO access key. Must be set to a secure value in production |
| `MINIO_SECRET_KEY` | N/A | **Required for self-hosted deployments.** MinIO secret key. Must be set to a secure value in production |
| `MINIO_BUCKET` | `voice-audio` | Bucket name for audio files |
| `MINIO_SECURE` | `false` | Use HTTPS for MinIO connections |

### AWS S3 (alternative)

| Variable | Default | Description |
|---|---|---|
| `ENABLE_AWS_S3` | `false` | Set to `true` to use AWS S3 instead of MinIO |
| `S3_BUCKET` | `null` | S3 bucket name |
| `S3_REGION` | `us-east-1` | AWS region |
| `S3_ENDPOINT_URL` | `null` | Custom S3 endpoint for S3-compatible servers (e.g. `https://s3.example.com`). Leave unset for AWS. |
| `S3_SIGNATURE_VERSION` | `null` | Signing version. Unset uses botocore's default; set `s3v4` for servers that require SigV4. |
| `S3_ADDRESSING_STYLE` | `null` | `auto` (default), `path`, or `virtual`. Many S3-compatible servers and TLS setups require `path`. |

Credentials come from the standard `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` environment variables.

#### S3-compatible servers (MinIO, rustfs, Ceph, ...)

The S3 backend can target any S3-compatible server, not just AWS. Prefer it over the MinIO backend when you need **presigned URLs against a private bucket**: the MinIO backend returns plain unsigned object URLs and relies on the bucket being anonymously public-readable, whereas the S3 backend issues real presigned URLs so the bucket can stay private.

To use it, set `ENABLE_AWS_S3=true` and point it at your server with the `S3_*` overrides above. For example, against a RustFS-compatible endpoint:

```bash
ENABLE_AWS_S3=true
S3_BUCKET=voice-audio
S3_REGION=us-east-1
S3_ENDPOINT_URL=https://s3.example.com
S3_SIGNATURE_VERSION=s3v4   # rustfs rejects SigV2 with SignatureDoesNotMatch
S3_ADDRESSING_STYLE=path    # rustfs and most non-AWS TLS certs require path-style
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
```

<Note>
Presigned URLs point at `S3_ENDPOINT_URL`, so that host must be reachable from the browser. Because browsers fetch transcripts cross-origin, the bucket also needs a CORS rule allowing your app's origin for `GET`/`HEAD` — configure this on the storage server (e.g. via `PutBucketCors`), not in Menace Voice.
</Note>

---

## WebRTC

| Variable | Default | Description |
|---|---|---|
| `ENABLE_COTURN` | `false` | Whether this deployment runs a TURN server. Reported to browsers as `turn_enabled` on `/api/v1/health` and in the embed widget config; when `false` clients skip the TURN credential request and connect over STUN, and the server returns 503 from the credential endpoints. Set to `true` wherever coturn is running — the setup scripts and Helm chart do this for you |
| `TURN_HOST` | `PUBLIC_HOST`, else `localhost` | TURN server hostname for WebRTC NAT traversal. Derives from `PUBLIC_HOST`; set explicitly only when TURN runs on a separate host |
| `TURN_PORT` | `3478` | TURN server port |
| `TURN_TLS_PORT` | `5349` | TURN server TLS port |
| `TURN_SECRET` | `null` | **Required for WebRTC.** Shared secret for TURN credential generation |
| `TURN_CREDENTIAL_TTL` | `86400` | TURN credential validity in seconds (default: 24h) |
| `FORCE_TURN_RELAY` | `false` | Diagnostic flag. When `true`, restricts ICE to relay-only candidates on both server (SDP filter) and browser (`iceTransportPolicy: 'relay'`). Use to verify TURN connectivity end-to-end — calls fail cleanly if TURN is misconfigured instead of silently falling back to a direct path. |

---

## Tracing (Langfuse)

| Variable | Default | Description |
|---|---|---|
| `LANGFUSE_HOST` | `null` | Langfuse server URL |
| `LANGFUSE_PUBLIC_KEY` | `null` | Langfuse public key |
| `LANGFUSE_SECRET_KEY` | `null` | Langfuse secret key |
| `LANGFUSE_PROJECT_ID` | `null` | Langfuse project ID, required with environment credentials to generate trace URLs |
| `LANGFUSE_TRACES_PUBLIC` | `false` | When `true`, traces are marked public in Langfuse — anyone holding a trace URL can read it without logging in. Leave unset to keep traces visible only to your Langfuse project members. |

Tracing activates automatically as soon as credentials are available — either via these environment variables (applied to all organizations) or per-organization in the UI under **Platform Settings**. Environment credentials require `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and `LANGFUSE_PROJECT_ID`. If neither is set, spans are dropped silently. See the [Tracing guide](/configurations/tracing) for setup instructions.

---

## Telephony

Carriers dial the media WebSocket back at `/api/v1/telephony/ws/{workflow_id}/{organization_id}/{workflow_run_id}`. Those ids travel in a caller-visible URL, so on their own they are a guessable capability. Setting a secret makes Menace Voice sign that URL with an HMAC and verify the signature when the socket opens.

The signature travels as a trailing path segment — `/api/v1/telephony/ws/{workflow_id}/{organization_id}/{workflow_run_id}/{token}` — because carriers do not reliably forward query strings. Twilio [documents that its `<Stream>` url "does not support query string parameters"](https://www.twilio.com/docs/voice/twiml/stream) and drops them outright. Asterisk ARI is the exception: it connects to `/api/v1/telephony/ws/ari` and passes `token` as a query parameter alongside its other routing values, which works because Asterisk builds that URL itself.

| Variable | Default | Description |
|---|---|---|
| `TELEPHONY_WS_TOKEN_SECRET` | `null` | Secret used to sign the media WebSocket URL. Unset leaves URLs unchanged and the check disabled |
| `TELEPHONY_WS_TOKEN_ENFORCE` | `false` | Reject connections whose token is missing or invalid (WebSocket close `4401`) |

Roll it out in two steps so no call is dropped:

1. Set `TELEPHONY_WS_TOKEN_SECRET` on **every** process that places or receives calls — the `api` service and, if you run Asterisk, the `ari-manager`. They must share the same value: one mints the token, the other verifies it. Calls keep connecting either way; anything unverifiable is logged as `UNVERIFIED media socket`.
2. Once those warnings stop, set `TELEPHONY_WS_TOKEN_ENFORCE=true` to start rejecting them.

Setting `TELEPHONY_WS_TOKEN_ENFORCE` without a secret does nothing at all — the check is skipped entirely rather than rejecting every connection.

<Note>
The signed URL is a bearer credential, and it is not hidden: the token appears in full in the `api` and nginx access logs, and in carrier-side request logs (Twilio's debugger, Telnyx's request inspector). Anyone who can read those logs can open the media socket for a run that is still in `initialized`. Treat log access as socket access.
</Note>

---

## Monitoring

| Variable | Default | Description |
|---|---|---|
| `SENTRY_DSN` | `null` | Sentry DSN for error tracking |
| `ENABLE_TELEMETRY` | `false` | Enable anonymous telemetry collection |

---

## Logging

| Variable | Default | Description |
|---|---|---|
| `LOG_LEVEL` | `DEBUG` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `LOG_FILE_PATH` | `null` | Write logs to this file path (in addition to stdout) |
| `LOG_ROTATION_SIZE` | `100 MB` | Rotate log file when it reaches this size |
| `LOG_RETENTION` | `7 days` | How long to keep rotated log files |
| `LOG_COMPRESSION` | `gz` | Compression format for rotated logs |
| `SERIALIZE_LOG_OUTPUT` | `false` | Output logs as JSON (useful for log aggregation) |

---

## Campaigns

Controls concurrency for [Campaigns](/core-concepts/campaigns), Menace Voice's bulk outbound calling feature.

| Variable | Default | Description |
|---|---|---|
| `DEFAULT_ORG_CONCURRENCY_LIMIT` | `10` | Maximum concurrent active calls per organization (values below 1 are clamped to 1) |

---

## Further Customisation

The variables documented here cover the standard configuration surface. For advanced customisation — such as integrating additional services or tuning internal behaviour — consult the relevant module alongside `api/constants.py` to understand how each variable is consumed.
