> ## Documentation Index
> Fetch the complete documentation index at: https://docs.causeloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Variables

> Complete reference for every environment variable Causeloop reads, grouped by category, with production requirements called out.

All configuration is supplied through environment variables. Copy `.env.example` to `.env` and fill in the values before starting the server. When `DATABASE_URL` is not set, the app uses an in-memory store — no database is required for local development or demos.

<Warning>
  The following variables are **production-critical**. The application starts with the development defaults, but using them in production is a security risk.

  * `JWT_SECRET` — change from `dev-secret-change-me` before any real traffic
  * `CAUSELOOP_MASTER_KEY` — must be set when `DATABASE_URL` is set; encrypts all per-workspace data keys
</Warning>

## Core server

| Variable       | Default                 | Required       | Description                                                                                                                        |
| -------------- | ----------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `PORT`         | `4000`                  | No             | HTTP listen port. Caddy and `docker-compose.yml` proxy to this port.                                                               |
| `JWT_SECRET`   | `dev-secret-change-me`  | **Yes (prod)** | HS256 signing key for JWTs issued at `/auth/token`. Generate with `python3 -c "import secrets; print(secrets.token_urlsafe(48))"`. |
| `CORS_ORIGINS` | `http://localhost:3000` | **Yes (prod)** | Comma-separated list of allowed CORS origins. Set to your frontend domain in production, e.g. `https://app.causeloop.ai`.          |

## Authentication — identity provider

`AUTH_PROVIDER` selects which upstream identity provider `POST /auth/exchange` verifies tokens against. It is pluggable — `workos` (the default, and what the Causeloop frontend uses via `@workos-inc/authkit-nextjs`), `auth0`, `okta`, `oidc` (generic), or `none` (no external provider — the dev fallback below).

| Variable              | Default   | Required                            | Description                                                                                                                                                                                                                 |
| --------------------- | --------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_PROVIDER`       | `workos`  | No                                  | `workos` \| `auth0` \| `okta` \| `oidc` \| `none`.                                                                                                                                                                          |
| `WORKOS_CLIENT_ID`    | *(empty)* | Required for `AUTH_PROVIDER=workos` | Your WorkOS AuthKit client ID. Also derives the JWKS URL (`https://api.workos.com/sso/jwks/<client_id>`) and the issuer (`https://api.workos.com`) unless overridden below.                                                 |
| `WORKOS_API_KEY`      | *(empty)* | Required for `AUTH_PROVIDER=workos` | Your WorkOS API key (server-side; used by the frontend's AuthKit integration, not read directly by the exchange verifier).                                                                                                  |
| `AUTH_ISSUER`         | *(empty)* | No                                  | Overrides the provider's default issuer. Required for `auth0` / `okta` / `oidc`.                                                                                                                                            |
| `AUTH_JWKS_URL`       | *(empty)* | No                                  | Overrides the provider's default JWKS URL.                                                                                                                                                                                  |
| `AUTH_AUDIENCE`       | *(empty)* | No                                  | Expected `aud` claim. Leave empty to skip audience validation.                                                                                                                                                              |
| `AUTH_CLIENT_ID`      | *(empty)* | No                                  | Expected `client_id` claim on the verified token (checked in addition to signature/issuer/audience).                                                                                                                        |
| `AUTH_ALLOW_JIT`      | `false`   | No                                  | Together with `AUTH_REQUIRE_INVITE=false`, allows domain-based just-in-time provisioning for a workspace that opts in (`jit_policy: "domain"` + a verified domain). Off by default — exchange is deny-by-default otherwise. |
| `AUTH_REQUIRE_INVITE` | `true`    | No                                  | When true, an identity with no existing membership and no pending invitation is denied (`403`), even with `AUTH_ALLOW_JIT=true`.                                                                                            |

<Note>
  With no `AUTH_PROVIDER` configured (or `AUTH_PROVIDER=none`), `/auth/exchange` falls back to a seed-user dev path — but **only** when `ENVIRONMENT` is not `production`. In production with no provider configured, exchange returns `503`. See [Local development](/deploy-security/local-development) for the dev-token flow.
</Note>

## LLM providers

Leave both API key variables empty to run in offline mock mode — the mock provider returns plausible stub responses and is suitable for local development and tests.

| Variable                 | Default                 | Required | Description                                                                                            |
| ------------------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `ANTHROPIC_API_KEY`      | *(empty)*               | No       | Anthropic API key. When set, Anthropic Claude is available as an LLM provider.                         |
| `OPENAI_API_KEY`         | *(empty)*               | No       | OpenAI API key. When set, GPT-4o models are available.                                                 |
| `LLM_PROVIDER_ORDER`     | `anthropic,openai,mock` | No       | Comma-separated resolution order. The first provider with a usable key is selected.                    |
| `USE_MOCK_LLM`           | `false`                 | No       | Force the offline mock regardless of which API keys are set. Useful for CI or air-gapped environments. |
| `OPENAI_MODEL_REASONING` | `gpt-4o`                | No       | OpenAI model used for multi-step reasoning tasks (pattern analysis, predictions).                      |
| `OPENAI_MODEL_FAST`      | `gpt-4o-mini`           | No       | OpenAI model used for quick completions (summaries, labels).                                           |

## Database

| Variable            | Default   | Required                 | Description                                                                                                                                                                          |
| ------------------- | --------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DATABASE_URL`      | *(unset)* | No (prod: **Yes**)       | PostgreSQL connection string. When unset, the app uses an in-memory store. For Neon: `postgresql://<user>:<pass>@<endpoint>.neon.tech/<db>?sslmode=require`.                         |
| `POSTGRES_PASSWORD` | *(unset)* | Only with docker-compose | Used by `docker-compose.yml` to set the Postgres superuser password and inject `DATABASE_URL` into the API container. Do not set this if you are using an external managed database. |

<Tip>
  For local Postgres: `DATABASE_URL=postgresql://localhost:5432/causeloop`

  For Neon (hosted, SSL required): `DATABASE_URL=postgresql://<user>:<pass>@<endpoint>.neon.tech/<db>?sslmode=require`
</Tip>

## Encryption

| Variable               | Default   | Required                           | Description                                                                                                                                                                                                                                         |
| ---------------------- | --------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CAUSELOOP_MASTER_KEY` | *(empty)* | **Yes when `DATABASE_URL` is set** | Base64-encoded 32-byte AES-256 key (the Key Encryption Key). Wraps per-workspace data keys stored in `workspace_keys`. **Never commit this value.** Generate with: `python3 -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"` |

<Warning>
  If `DATABASE_URL` is set but `CAUSELOOP_MASTER_KEY` is empty, the application will raise a `KeyError_` on the first request that touches an encrypted column. Generate and store this secret securely before pointing the app at a real database.
</Warning>

## Model provisioning & inference

Flags gating the [provisioning portal](/deploy-security/provisioning-portal),
[model lifecycle](/deploy-security/model-lifecycle), and
[inference/queue](/deploy-security/inference-and-queue) subsystems. All are additive/rollback-safe:
flipping any of these off does not delete data — it only stops a code path from running. A model
already trained, evaluated, or hosted stays exactly as it was; only new activity is affected.

| Variable                      | Default                          | Rollback semantics                                                                                                                                                                                                                                                                                                                     |
| ----------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PROVISIONING_PORTAL_ENABLED` | `false`                          | Off = every `/v1/provisioning/client-profiles`, `training-runs`, `models`, `services` route returns `404` (not `403` — the portal's existence isn't advertised). Flip back on and every existing row/artifact is exactly where it was.                                                                                                 |
| `WM2_ARTIFACTS`               | `false`                          | Off = trains produce the older wm\@1 manifest shape (13 fixed keys, no extra files) — byte-unchanged from before wm\@2 existed. On = the additional files/blocks documented in [Inference & Queue](/deploy-security/inference-and-queue#the-wm2-artifact). Existing wm\@1 artifacts are unaffected either way; nothing re-writes them. |
| `EVAL_REQUIRED`               | `true`                           | Off = `hosting.create_service` (E1 attach) skips the "artifact must be eval\_passed/approved/overridden" check entirely. Does **not** disable the evaluation UI or R-L1's separate approval gate — only the hosting-attach guard.                                                                                                      |
| `LOOP_INFERENCE_ENABLED`      | `false`                          | Off = `forge_issue` never calls inline inference; issues ingest without an assignment. On, additionally gated on `QUEUE_INGEST=="off"` — see below.                                                                                                                                                                                    |
| `QUEUE_INGEST`                | `"off"`                          | `off` = inline synchronous inference (the tested default fallback). `redis` = outbox → relay → stream → consumer. Switching back to `off` mid-flight leaves any already-published-but-unconsumed stream messages unread until a consumer picks them up later, or forever if none ever does — not silently lost, just parked.           |
| `INFERENCE_PROFILE`           | `"free"`                         | `free` = shared/in-process inference, zero Render calls. `prod` = real per-client Render deploys (respx-tested only in this environment — never run against a live account). Switching an already-hosted client's profile does not retroactively touch its existing service.                                                           |
| `MODEL_STORE_URI`             | `file://./var/causeloop/models`  | `file://` = local disk (default, tested). `s3://...` = `S3ModelStore` (real boto3 code, moto-tested; not yet pointed at a live account in this environment). Switching backends does not migrate existing artifacts between them.                                                                                                      |
| `UPLOAD_STORE_URI`            | `file://./var/causeloop/uploads` | `file://` = local disk (default, tested). `s3://...` routes to `SupabaseUploadStore`, which is a **stub** — every method raises `NotImplementedError`. Do not set this to `s3://` expecting uploads to work.                                                                                                                           |
| `UPLOAD_MAX_BYTES`            | `52428800` (50 MB)               | Raising this only affects future uploads; already-stored files are unaffected.                                                                                                                                                                                                                                                         |

<Warning>
  `INFERENCE_PROFILE=prod` and `UPLOAD_STORE_URI=s3://...` (Supabase Storage) are **configuration
  paths, not proven capabilities** — neither has been exercised against a live account in this
  environment. See [Inference & Queue → Config-only
  paths](/deploy-security/inference-and-queue#config-only-paths-never-exercised-against-a-live-account)
  before describing either as production-ready.
</Warning>

## Frontend integration

Add these to your Next.js (or any frontend) `.env.local`:

```env theme={null}
NEXT_PUBLIC_USE_MOCK=0
NEXT_PUBLIC_API_BASE_URL=https://api.causeloop.ai/v1
NEXT_PUBLIC_WS_URL=wss://api.causeloop.ai/v1/stream
```

For local development:

```env theme={null}
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/v1
NEXT_PUBLIC_WS_URL=ws://localhost:4000/v1/stream
```

The API client reads the JWT from `localStorage['causeloop_token']`.

## Full `.env.example`

```env theme={null}
PORT=4000
JWT_SECRET=dev-secret-change-me
CORS_ORIGINS=http://localhost:3000

# Leave LLM keys empty to use the offline mock provider:
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
LLM_PROVIDER_ORDER=anthropic,openai,mock
USE_MOCK_LLM=false
OPENAI_MODEL_REASONING=gpt-4o
OPENAI_MODEL_FAST=gpt-4o-mini

# WorkOS AuthKit — used to verify access tokens at POST /v1/auth/exchange
WORKOS_CLIENT_ID=client_...
WORKOS_API_KEY=sk_...

# Postgres (when set, the app uses the Postgres repo instead of in-memory)
# DATABASE_URL=postgresql://localhost:5432/causeloop

# Envelope-encryption master key (base64 of 32 bytes). REQUIRED when DATABASE_URL is set.
# Generate: python3 -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"
CAUSELOOP_MASTER_KEY=
```
