# FourIA — Environment Variables Reference

Complete reference of all environment variables used across FourIA Workers. Derived from `apps/fouria/src/gateway/env.ts` and `apps/fouria/.dev.vars.example`.

---

## Variable Classification

FourIA uses three classifications for configuration values:

| Classification      | Storage                                   | Binding Type  | Managed By                   | Examples                                    |
| ------------------- | ----------------------------------------- | ------------- | ---------------------------- | ------------------------------------------- |
| **Instance Secret** | CF encrypted `secret_text`                | `secret_text` | lerma provisioner            | Gateway token, CDP secret, AI keys, Stripe  |
| **Variable**        | `wrangler.jsonc` / WFP `plain_text`       | `plain_text`  | Provisioner / IaC            | DEV_MODE, URLs, account IDs, sandbox config |
| **Dynamic Secret**  | F4E1-encrypted R2 object in BACKUP_BUCKET | —             | End-user via integrations UI | Channel tokens, OAuth, custom integrations  |

### Instance Secrets

These are automatically generated and deployed by the `lerma` provisioner as `secret_text` bindings on each tenant User Worker. For the full inventory of required secrets, see the secrets manifest.

| Secret                   | Description                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `MOLTBOT_GATEWAY_TOKEN`  | Auto-generated gateway token for CLI auth. Mapped to `OPENCLAW_GATEWAY_TOKEN` in container. |
| `CF_ACCESS_PLATFORM_AUD` | Platform-level Access AUD (always present, from dispatch worker).                           |
| `CF_ACCESS_CLIENT_AUD`   | Per-tenant Access AUD (optional, provisioned by lerma).                                     |

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

### Variables (Non-Secret Config)

Operational configuration, safe to store in `wrangler.jsonc` or `.dev.vars`. Deployed as `plain_text` bindings by the provisioner.

| Variable                      | Container | Required | Description                                                                                                                                                                                                                       |
| ----------------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEV_MODE`                    | Yes†      | No       | Skips CF Access auth + bypasses device pairing. Local dev only.                                                                                                                                                                   |
| `E2E_TEST_MODE`               | Yes‡      | No       | Skips CF Access auth but keeps device pairing. For automated E2E.                                                                                                                                                                 |
| `DEBUG_ROUTES`                | No        | No       | Enables `/debug/*` endpoints (ROOT role only). Off in production.                                                                                                                                                                 |
| `DEMO_MODE`                   | No        | No       | Enables demo-specific UI behaviors (read-only, sample data).                                                                                                                                                                      |
| `SANDBOX_SLEEP_AFTER`         | No        | No       | Container sleep timeout: `never` (default) or `10m`, `1h`, etc.                                                                                                                                                                   |
| `SANDBOX_INSTANCE_TIMEOUT_MS` | No        | No       | Bound for container VM provisioning (default: `20000`). Fail-fast detection of a degraded/off container.                                                                                                                          |
| `SANDBOX_PORT_TIMEOUT_MS`     | No        | No       | Bound for container port readiness (default: `90000`). Keep generous — gateway cold start is slow.                                                                                                                                |
| `SANDBOX_POLL_INTERVAL_MS`    | No        | No       | Poll interval for container readiness (default: `300`).                                                                                                                                                                           |
| `SANDBOX_LOG_LEVEL`           | No        | No       | Container log level: `debug`, `info`, `warn`, `error`                                                                                                                                                                             |
| `SANDBOX_LOG_FORMAT`          | No        | No       | Container log format: `pretty` or `json`                                                                                                                                                                                          |
| `SANDBOX_TRANSPORT`           | No        | No       | Container transport: `rpc`                                                                                                                                                                                                        |
| `WORKER_URL`                  | Yes       | No       | Public Worker URL (used for CDP and OTel endpoint)                                                                                                                                                                                |
| `WORKER_NAME`                 | Yes       | No       | Worker name for config bucket key resolution                                                                                                                                                                                      |
| `CF_AI_GATEWAY_ACCOUNT_ID`    | Yes       | Yes\*    | Cloudflare account ID for AI Gateway URL construction                                                                                                                                                                             |
| `CF_AI_GATEWAY_GATEWAY_ID`    | Yes       | Yes\*    | AI Gateway ID for URL construction                                                                                                                                                                                                |
| `CF_AI_GATEWAY_CUSTOM_DOMAIN` | No        | No       | AI Gateway custom domain (e.g. `ai.example.com`). Routes unified billing through `https://<domain>/compat` instead of `api.cloudflare.com`; required for Cloudflare Access identity-aware controls and User Insights attribution. |
| `CF_ACCESS_TOKEN`             | No        | Yes\*    | Cloudflare Access user JWT forwarded as `cf-access-token` on an Access-protected custom domain. AI Gateway records `cf.user_id`, which User Insights uses to attribute spend per identity.                                        |
| `CF_ACCESS_CLIENT_ID`         | No        | Yes\*    | Cloudflare Access service-token client ID (headless alternative; service tokens do not produce `cf.user_id`).                                                                                                                     |
| `CF_ACCESS_CLIENT_SECRET`     | No        | Yes\*    | Cloudflare Access service-token client secret.                                                                                                                                                                                    |
| `CF_ACCESS_TEAM_DOMAIN`       | No        | Yes\*    | Cloudflare Access team domain (e.g., `myteam.cloudflareaccess.com`)                                                                                                                                                               |
| `CLOUDFLARE_ACCOUNT_ID`       | Yes       | No       | Cloudflare account ID. Read by the Sandbox SDK for R2 presigned URL signing (backup/restore). Legacy `CF_ACCOUNT_ID` is deprecated (still mapped into the container by `buildEnvVars`).                                           |
| `BACKUP_BUCKET_NAME`          | Yes§      | No       | R2 backup bucket name (default: `fouria-data`). Required by the Sandbox SDK presigned-URL backup path; mapped to `R2_BUCKET_NAME` in container.                                                                                   |
| `BACKUP_MAX_VERSIONS`         | No        | No       | Max squashfs snapshot versions to retain (default: 3)                                                                                                                                                                             |
| `BACKUP_INTERVAL_MINUTES`     | No        | No       | How often to sync backup snapshots (default: 5)                                                                                                                                                                                   |

† Mapped to `OPENCLAW_DEV_MODE` in container.
‡ Mapped to `OPENCLAW_E2E_TEST_MODE` in container.
§ Mapped to `R2_BUCKET_NAME` in container.

**Mode behavior summary:**

| Mode            | CF Access Auth | Device Pairing | Debug Routes | Write API |
| --------------- | -------------- | -------------- | ------------ | --------- |
| Production      | Required       | Required       | Disabled     | RBAC      |
| `DEV_MODE`      | Bypassed       | Bypassed       | Configurable | ROOT      |
| `E2E_TEST_MODE` | Bypassed       | Required       | Configurable | ROOT      |
| `DEMO_MODE`     | Required       | Required       | Disabled     | Read-only |

---

## Dynamic Secrets (F4E1-Encrypted R2 Storage)

Channel tokens, tool OAuth credentials, and custom integration secrets are managed by end-users through the **integrations dashboard**. They are stored as F4E1-encrypted JSON objects in the `BACKUP_BUCKET` R2 bucket under the `secrets/<customerSlug>/` prefix.

**Secret categories:** `ai_provider`, `channel`, `integration`

The encryption uses the shared `BACKUP_ENCRYPTION_KEY` (same master key used for backup snapshots) with per-object HKDF-derived AES-256-GCM keys (domain-separated via `fouria-r2-secrets/v1`). The master key never leaves the Worker and is never passed to the container.

Unlike the old vault, there is no unlock/lock mechanism: secrets are always accessible when the key is configured. The `/lock` endpoint is a no-op kept for client compatibility. The WebSocket API (`/ws`) has been removed (returns 410 Gone).

> See `apps/fouria/src/services/secrets/R2SecretsStore.ts` for the full implementation.

---

## AI Provider Configuration

At least one provider must be configured. Priority: CF AI Gateway > Anthropic > OpenAI.

### CI-Only Secret Mapping

The CI workflow maps GitHub secrets (prefixed with `E2E_`) to their corresponding env variables. See `.github/workflows/test.yml` for the authoritative mapping.

| GitHub Secret                       | Maps To                         |
| ----------------------------------- | ------------------------------- |
| `E2E_CLOUDFLARE_AI_GATEWAY_API_KEY` | `CLOUDFLARE_AI_GATEWAY_API_KEY` |
| `E2E_CF_AI_GATEWAY_ACCOUNT_ID`      | `CF_AI_GATEWAY_ACCOUNT_ID`      |
| `E2E_CF_AI_GATEWAY_GATEWAY_ID`      | `CF_AI_GATEWAY_GATEWAY_ID`      |
| `E2E_TEARDOWN_API_TOKEN`            | `CLOUDFLARE_TEARDOWN_API_TOKEN` |

> There is no GitHub secret named `CLOUDFLARE_AI_GATEWAY_API_KEY`. Staging and
> production Lerma deploys (`.github/workflows/deploy-dashboard.yml`) both read
> `E2E_CLOUDFLARE_AI_GATEWAY_API_KEY` and deploy it as the
> `CLOUDFLARE_AI_GATEWAY_API_KEY` runtime env. Referencing a non-existent
> `secrets.CLOUDFLARE_AI_GATEWAY_API_KEY` expands to empty and ships a dashboard
> whose tenant provisioning fails with `:missing_ai_gateway_credential` — the
> deploy workflow fails fast when the secret is empty.

> `E2E_TEARDOWN_API_TOKEN` is used only by the E2E teardown for destructive cleanup (`wrangler delete` + R2 bucket empty/delete + container/Access/service-token deletes). It must carry Workers Scripts, Workers KV Storage, Workers R2 Storage, Cloudflare Containers, and Access (Apps + Service Tokens) **Edit** permissions. When unset, teardown falls back to `CLOUDFLARE_API_TOKEN` (`DEPLOY_WORKER_API_TOKEN` in CI).
>
> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

### Runtime AI Credentials

| Variable                        | Class                | Container | Description                                                                                                                                                                 |
| ------------------------------- | -------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLOUDFLARE_AI_GATEWAY_API_KEY` | Platform Secret      | Yes       | Real AI-scoped Cloudflare credential supplied by Lerma. It is never generated randomly per tenant.                                                                          |
| `CLOUDFLARE_API_TOKEN`          | Tenant Runtime Alias | Yes       | Bearer credential for the AI Gateway REST API. Lerma derives this tenant binding from `CLOUDFLARE_AI_GATEWAY_API_KEY`; it is not Lerma's administrative provisioning token. |

The native `cloudflare-ai-gateway` provider uses `CLOUDFLARE_AI_GATEWAY_API_KEY`. The unified-billing plugin uses `CLOUDFLARE_API_TOKEN` for `api.cloudflare.com/client/v4/accounts/{account}/ai/v1` requests. When `CF_AI_GATEWAY_CUSTOM_DOMAIN` is set, the plugin switches to `https://{domain}/compat` and authenticates with `CF_ACCESS_TOKEN` (or a service token) when the domain is behind Cloudflare Access — this is what enables User Insights identity attribution via `cf.user_id`.

\* At least one provider must be configured.

---

## Authentication & Access Control

| Variable                 | Class           | Container | Required | Description                                                                 |
| ------------------------ | --------------- | --------- | -------- | --------------------------------------------------------------------------- |
| `MOLTBOT_GATEWAY_TOKEN`  | Instance Secret | Yes†      | Yes      | Gateway token for CLI auth. Auto-generated per tenant by lerma provisioner. |
| `CF_ACCESS_PLATFORM_AUD` | Instance Secret | No        | Yes*     | Platform-level Access AUD (always present, from dispatch worker).           |
| `CF_ACCESS_CLIENT_AUD`   | Instance Secret | No        | No       | Per-tenant Access AUD (optional, provisioned by lerma).                     |
| `CF_ACCESS_TEAM_DOMAIN`  | Variable        | No        | Yes*     | Cloudflare Access team domain (e.g., `myteam.cloudflareaccess.com`).        |

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

\* Required for production. Not required when `DEV_MODE=true` or `E2E_TEST_MODE=true`.
† Mapped to `OPENCLAW_GATEWAY_TOKEN` by `buildEnvVars()`.

---

## Persistence & Storage

| Variable                     | Class    | Container | Required    | Description                                                                                                                                                                                                                                         |
| ---------------------------- | -------- | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLOUDFLARE_ACCOUNT_ID`      | Variable | Yes       | No          | Cloudflare account ID. Read by the Sandbox SDK for R2 presigned URL signing (backup/restore). Legacy `CF_ACCOUNT_ID` is deprecated (still mapped into the container by `buildEnvVars`).                                                             |
| `BACKUP_BUCKET_NAME`         | Variable | Yes†      | No          | R2 backup bucket name (default: `fouria-data`). Required by the Sandbox SDK presigned-URL backup path alongside the `BACKUP_BUCKET` binding; mapped to `R2_BUCKET_NAME` in container.                                                               |
| `R2_ACCESS_KEY_ID`           | Secret   | Yes       | Yes         | R2 access key. Required by the Sandbox SDK presigned-URL backup path. Resolved per-instance by the lerma provisioner (shared lerma credential, or a generated per-instance token) and injected as a `secret_text` binding — never omitted silently. |
| `R2_SECRET_ACCESS_KEY`       | Secret   | Yes       | Yes         | R2 secret key, paired with `R2_ACCESS_KEY_ID`. Resolved per-instance by the lerma provisioner (see above).                                                                                                                                          |
| `BACKUP_MAX_VERSIONS`        | Variable | No        | No          | Max squashfs snapshot versions to retain (default: 3)                                                                                                                                                                                               |
| `BACKUP_INTERVAL_MINUTES`    | Variable | No        | No          | How often to sync backup snapshots (default: 5)                                                                                                                                                                                                     |
| `BACKUP_ENCRYPTION_KEY`      | Secret   | No        | Recommended | Base64 32-byte AES-256 master key for client-side R2 backup encryption (F4E1). Worker-side only, never passed to the container. Generate: `openssl rand -base64 32`                                                                                 |
| `BACKUP_ENCRYPTION_REQUIRED` | Variable | No        | No          | Set to `true` to fail closed (skip backup + error) when `BACKUP_ENCRYPTION_KEY` is missing (default: false, legacy plaintext + warning)                                                                                                             |

† Mapped to `R2_BUCKET_NAME` in container.

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

---

## Cloudflare Tunnel

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

---

## Browser Automation (CDP)

| Variable     | Class    | Container | Required | Description                                        |
| ------------ | -------- | --------- | -------- | -------------------------------------------------- |
| `WORKER_URL` | Variable | Yes       | No       | Public Worker URL (used for CDP and OTel endpoint) |

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

---

## Container Rescue (`/debug/rescue-container`)

Recovers a container stuck in Error mode (e.g. the `fouria:unstable-<sha>` image it references was deleted by the stale-image cleanup cron). The tool probes the container via the Sandbox SDK, re-points the application image to the expected tag through the Cloudflare Containers API (rollout), and destroys the stuck container so the platform provisions a fresh one. See `apps/fouria/src/diagnostics.ts`.

| Variable                   | Class          | Container | Required | Description                                                                                                                                                             |
| -------------------------- | -------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLOUDFLARE_API_TOKEN`     | Runtime Secret | Yes       | No       | AI Gateway REST Bearer token on tenant workers. On ROOT/ops deployments the same binding may also be used by `/debug/rescue-container` when it has **Containers:Edit**. |
| `CONTAINER_APPLICATION_ID` | Variable       | No        | No       | Cloudflare Containers application id for this worker's Sandbox container (set by the provisioner).                                                                      |
| `CONTAINER_IMAGE_TAG`      | Variable       | No        | No       | Expected container image tag (e.g. `unstable-<sha>`); used to derive the repair target `registry.cloudflare.com/<account>/fouria:<tag>`.                                |
| `CONTAINER_IMAGE_NAME`     | Variable       | No        | No       | Container image repo name for the tenant sandbox image (default: `fouria`).                                                                                             |

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

---

| Variable                              | Class           | Container | Required | Description                                             |
| ------------------------------------- | --------------- | --------- | -------- | ------------------------------------------------------- |
| `OTEL_SHARED_SECRET`                  | Instance Secret | Yes       | No       | Shared secret for OTel metric ingest authentication     |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Auto            | Yes       | Auto     | Auto-derived from `WORKER_URL` + `/api/otel/v1/metrics` |
| `OTEL_EXPORTER_OTLP_PROTOCOL`         | Auto            | Yes       | Auto     | Auto-set to `http/json`                                 |
| `OTEL_SERVICE_NAME`                   | Auto            | Yes       | Auto     | Auto-set to customer slug or worker name                |

---

## Feature Flags

| Variable                 | Class    | Container | Required | Description                                                                                  |
| ------------------------ | -------- | --------- | -------- | -------------------------------------------------------------------------------------------- |
| `MOLTLAZY_FEATURE_FLAGS` | Auto     | Yes       | Auto     | JSON feature flags for moltlazy config generation (auto-generated).                          |
| `WORKER_NAME`            | Variable | Yes       | No       | Worker name for config bucket key resolution.                                                |
| `STARTUP_LOG_VERBOSITY`  | Variable | Yes       | No       | Clean boot-log verbosity for the loading page: `steps` \| `detailed` (default) \| `verbose`. |

---

## Stripe Billing Integration

Instance secrets for the Stripe subscription billing and payment management features in the Admin UI. Only accessed by the Worker (not the container).

| Variable                 | Class           | Container | Required | Description                                                                           |
| ------------------------ | --------------- | --------- | -------- | ------------------------------------------------------------------------------------- |
| `STRIPE_SECRET_KEY`      | Instance Secret | No        | No       | Stripe secret key (`sk_live_...`) for creating Checkout and Customer Portal sessions. |
| `STRIPE_PUBLISHABLE_KEY` | Instance Secret | No        | No       | Stripe publishable key (`pk_live_...`), exposed to the frontend for Stripe.js.        |

Billing data (plan tier, status, seats, credits, Stripe customer ID) is sourced from the Supabase database via the Dispatch Worker. The Worker does not persist billing state in Durable Objects.

## Communication Schema (Dispatch Worker → User Worker)

Billing metadata is passed from the Dispatch Worker to the User Worker via one of three resolution methods (in priority order):

| Priority      | Method                                       | Header / Source                             | Format                                                                               |
| ------------- | -------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------ |
| 1 (preferred) | Cloudflare for SaaS custom hostname metadata | `request.cf.hostMetadata`                   | JSON object — set via `PATCH /zones/:id/custom_hostnames/:id` with `custom_metadata` |
| 2             | Single metadata header                       | `X-FourIA-Metadata`                         | Base64url-encoded JSON of [`BillingMetadata`](#billingmetadata-schema)               |
| 3 (legacy)    | Individual headers                           | `X-FourIA-Plan`, `X-FourIA-SeatsUsed`, etc. | String values (deprecated)                                                           |

**Preferred approach (1):** Set `custom_metadata` on each customer's custom hostname via the Cloudflare API. The Dispatch Worker reads `request.cf.hostMetadata` and the User Worker reads it when `trusted_workers: true` is set on the dispatch namespace.

**Fallback approach (2):** The Dispatch Worker encodes the [`BillingMetadata`](#billingmetadata-schema) object as base64url JSON and sets it as the `X-FourIA-Metadata` header when forwarding the request.

### BillingMetadata Schema

```typescript
interface BillingMetadata {
  plan_tier: string; // "free" | "basic" | "pro" | "enterprise"
  plan_name: string; // Human-readable name
  billing_status: string; // "active" | "past_due" | "canceled" | "trialing" | "incomplete"
  seats_used: number; // Current seat count
  seats_max: number; // Plan seat limit
  credits_used: number; // Credits consumed this period
  credits_max: number; // Credit limit (0 = unlimited/pay-as-you-go)
  stripe_customer_id: string | null; // Stripe customer for payments
  period_end: string | null; // ISO 8601 — current billing period end
  trial_end: string | null; // ISO 8601 — trial end (if trialing)
}
```

### RBAC Role Resolution

The user's RBAC role is forwarded by the Dispatch Worker via the `X-FourIA-Role` header (one of `ROOT`, `ADMIN`, `BASE_USER`), when the Dispatch Worker can resolve it:

- **Paso4 ROOT users** are **always** `ROOT`. A user qualifies when their JWT `aud` is the platform AUD (the `*.fouria.io` wildcard Access application) **or** their email is a member of the shared `Paso4 Root Users` Access group. The group check matters because per-instance Access applications include the ROOT group policy (so ROOT operators are not locked out at `{slug}.fouria.io`); Cloudflare matches the most-specific application, so those tokens carry the per-instance AUD rather than the platform AUD. In staging this is the only permitted audience/identity.
- **Client users** (per-instance Access application AUDs) get their role only when `client_users.status = validated` and `client_user_instances` links them to the requested instance. Pending, rejected, production-only, or dev-only assignments do not cross environment boundaries.

The User Worker's `resolveCustomerRole` resolves the role in priority order:

1. JWT `aud` matches `CF_ACCESS_PLATFORM_AUD` → `ROOT`
2. `X-FourIA-Role` header (from the Dispatch Worker)
3. JWT `custom.role` claim (IdP-provided)
4. `LOCAL_ROLE_MAP` (local dev fallback)

---

## Dashboard Tenant Provisioning (`apps/lerma`)

Variables required by the Phoenix dashboard to create and decommission tenant User Workers.

### CF Access Local Development

Variables for local CF Access integration via cloudflared tunnel. See `apps/lerma/bin/setup-cf-access-local.sh` for one-time setup.

| Variable                         | Class           | Required | Description                                                                                                                                                                                   |
| -------------------------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CF_ACCESS_TEAM_DOMAIN`          | Variable        | Yes*     | Cloudflare Access team domain (e.g., `paso4`). Used by lerma's dashboard auth and deployed to tenant workers.                                                                                 |
| `CF_ACCESS_AUD`                  | Variable        | Yes*     | Dashboard's own Access AUD (JWT audience for lerma's dashboard). Populated by the deploy flow from `CF_ACCESS_LERMA_AUD`/`STAGING_CF_ACCESS_LERMA_AUD`.                                       |
| `CF_ACCESS_PLATFORM_AUD`         | Variable        | Yes*     | Platform-level Access AUD (Paso4 Root app). Deployed to every tenant worker as the `CF_ACCESS_PLATFORM_AUD` secret so its auth middleware can validate JWTs forwarded by the Dispatch Worker. |
| `CF_ACCESS_SERVICE_TOKEN_ID`     | Instance Secret | No       | CF Access service token Client ID for local CI/CD (non-interactive).                                                                                                                          |
| `CF_ACCESS_SERVICE_TOKEN_SECRET` | Instance Secret | No       | CF Access service token Client Secret for local CI/CD.                                                                                                                                        |
| `CLOUDFLARED_TUNNEL_TOKEN`       | Instance Secret | No       | Tunnel token for `lerma-local` (routes to port 4001).                                                                                                                                         |
| `SKIP_AUTH`                      | Variable        | No       | Set to `"true"` to bypass CF Access JWT verification (escape hatch).                                                                                                                          |

### Invitation Email

Lerma delivers invitation confirmation emails through Cloudflare Email Service over authenticated SMTP submission (`smtp.mx.cloudflare.net:465`, implicit TLS). It reuses the existing provisioning credential as the SMTP password, so no additional secret is required.

| Variable          | Class  | Required | Description                                                                                                                                                                                        |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LERMA_API_TOKEN` | Secret | Yes      | Cloudflare API token. Must include the **Email Sending: Edit** permission; used as the SMTP password (username is the literal `api_token`). The `From` domain must be onboarded for Email Sending. |

Local development continues to use Swoosh's local mailbox adapter; tests use the Swoosh test adapter.

### Provisioning Configuration

| Variable                               | Class    | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| -------------------------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLOUDFLARE_API_TOKEN`                 | ENV only | Yes      | API token with Workers:Edit, R2:Edit, Containers:Edit, and D1 edit permissions. **Containers:Edit is required** — the dashboard generates short-lived registry credentials (`POST /containers/registries/{host}/credentials`) to list versions, and uses R2:Edit to fetch the worker bundle from the `fouria-bundles` R2 bucket at provisioning time. Without it the calls fail with `403 Authentication error`.                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `CLOUDFLARE_ACCOUNT_ID`                | Variable | Yes      | Paso4 Cloudflare account ID for provisioning calls. Must be set or the registry-health check reports it missing and registry-credentials calls fail with `Could not route to /accounts/containers/registries/... — object identifier is invalid` (the account ID is interpolated into the request path). On the fouria User Worker it is a plain var: the Sandbox SDK reads it for R2 presigned URL signing (backup/restore) — missing it throws `InvalidBackupConfigError` (`Backup requires R2 presigned URL credentials`). The dashboard health check also validates that `CLOUDFLARE_API_TOKEN` grants the required permission groups (`Workers Scripts Write`, `DNS Write`, `Access: Apps and Policies Write`, `Workers Containers Write`), flagging a missing one as "Limited access". Legacy `CF_ACCOUNT_ID` is deprecated. |
| `CONTAINER_REGISTRY_HOST`              | Variable | No       | Cloudflare managed container registry host (default: `registry.cloudflare.com`). Mirrors `CLOUDFLARE_CONTAINER_REGISTRY` in wrangler.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `CONTAINER_IMAGE_NAME`                 | Variable | No       | Container image repo name for the tenant sandbox image (default: `fouria`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `CONTAINER_BUNDLE_R2_BUCKET`           | Variable | No       | R2 bucket that publishes the worker bundle `fouria.mjs` (default: `fouria-bundles`). CI uploads the bundle to `<version>/fouria.mjs` and the dashboard fetches it from R2 at provisioning time.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `FOURIA_DISPATCH_NAMESPACE`            | Variable | No       | Workers for Platforms dispatch namespace name (default: `fouria-dispatch`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `FOURIA_R2_PUBLIC_ENDPOINT`            | Variable | No       | R2 public endpoint template, e.g. `https://pub-{bucket_id}.r2.dev`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `ANALYTICS_DATASET`                    | Variable | No       | Analytics Engine dataset name (default: `fourai_cost_metrics`, staging: `fourai_cost_metrics_staging`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `CONTAINER_IMAGE`                      | Variable | No       | Container image reference for the tenant's container application (linked to the Sandbox DO namespace). Example: `registry.cloudflare.com/<account_id>/fouria:latest`. When unset, lerma derives it from the published bundle version as `registry.cloudflare.com/<account_id>/fouria:<image_tag>`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `CONTAINER_MAX_INSTANCES`              | Variable | No       | Max container instances for tenant container apps (default: `3`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `AI_GATEWAY_CREATE_DEDICATED`          | Variable | No       | Allow creating a dedicated per-tenant AI Gateway when the shared gateway pool has no capacity (default: `true`). Set to `false` to enforce reuse of pooled gateways — provisioning then fails with `no_ai_gateway_capacity` instead of creating a new gateway. See `docs/CLOUD-ARCHITECTURE.md` (AI Gateway reuse) and the dashboard at `/dashboard/ai-gateways`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `WORKER_OBSERVABILITY_ENABLED`         | Variable | No       | Enable Workers Observability (logs + traces) on deployed tenant User Workers (default: `true`). Mirrors `apps/fouria/wrangler.jsonc`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WORKER_OBSERVABILITY_SAMPLING_RATE`   | Variable | No       | Head sampling rate for traces on tenant User Workers, `0..1` (default: `1`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `WORKER_OBSERVABILITY_LOGS`            | Variable | No       | Enable logs on tenant User Workers (default: `true`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WORKER_OBSERVABILITY_INVOCATION_LOGS` | Variable | No       | Enable invocation logs on tenant User Workers (default: `true`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `WORKER_OBSERVABILITY_TRACES`          | Variable | No       | Enable traces on tenant User Workers (default: `true`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `CONTAINER_OBSERVABILITY_LOGS`         | Variable | No       | Enable log capture on the tenant's container application (default: `true`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `CONTAINER_REGIONS`                    | Variable | No       | Container application placement regions (deployment zone), comma-separated (default: `WEUR`). Valid values: `ENAM`, `WNAM`, `EEUR`, `WEUR`, `APAC`, `SAM`, `ME`, `OC`, `AFR`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `R2_TOKEN_STRATEGY`                    | Variable | No       | How lerma resolves the mandatory per-tenant R2 S3 backup credential: `auto` (default), `shared`, or `per_instance`. `auto` uses `R2_TOKEN_MAX_PER_ACCOUNT`: a known limit `<= 250` selects `shared`, `> 250` or unknown selects `per_instance`. `per_instance` mints a scoped R2 token per instance at provisioning. See `Lerma.Provisioning.R2Credentials`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `R2_TOKEN_MAX_PER_ACCOUNT`             | Variable | No       | Account API-token limit used by `:auto` (default: unknown → `:per_instance`). Cloudflare exposes no endpoint for the maximum, so pin a known value here if desired.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `R2_ACCESS_KEY_ID`                     | Secret   | Cond.    | Lerma-level shared R2 S3 access key, required when the effective strategy is `shared`. Injected into every tenant worker as a `secret_text` binding. Generate with `mix lerma.r2_token --all-buckets`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `R2_SECRET_ACCESS_KEY`                 | Secret   | Cond.    | Lerma-level shared R2 S3 secret key, paired with `R2_ACCESS_KEY_ID`. Required when the effective strategy is `shared`; provisioning fails with `:missing_shared_r2_credentials` otherwise.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |

### Health Monitoring

Lerma's `HealthMonitor` periodically probes each tenant's public `GET /api/health` endpoint and keeps `clients.status` and `instances.status` accurate. `/api/health` is the **unified** health endpoint (returns startup/readiness/liveness probes plus the canonical `status` and `processId`); `GET /api/status` is a legacy alias over the same snapshot. Both are exposed by every User Worker without auth and are made publicly reachable via path-scoped Cloudflare Access applications (`*.fouria.io/api/health`, `*.fouria.io/api/status`, see `apps/fouria/iac/access.tf`). Probing also cold-starts asleep containers (`ensureGateway`), so a stopped VM recovers on the next check — pass `?passive=1` (used by ROOT diagnostics) to probe without warming.

| Variable                           | Class    | Required | Description                                                                                                                                 |
| ---------------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `HEALTH_MONITOR_ENABLED`           | Variable | No       | Enable the periodic health monitor (default: `true`). Always disabled in test env.                                                          |
| `HEALTH_MONITOR_INTERVAL_MS`       | Variable | No       | Poll interval in milliseconds (default: `120000`).                                                                                          |
| `HEALTH_MONITOR_TIMEOUT_MS`        | Variable | No       | Per-probe HTTP timeout in milliseconds (default: `15000`).                                                                                  |
| `HEALTH_MONITOR_FAILURE_THRESHOLD` | Variable | No       | Consecutive failed probes before the client transitions `production → error` (default: `3`). A healthy probe flips it back to `production`. |
| `HEALTH_MONITOR_PLATFORM_DOMAIN`   | Variable | No       | Domain the tenant workers are served under (default: `fouria.io`).                                                                          |
| `HEALTH_MONITOR_SLUG_PREFIX`       | Variable | No       | Hostname prefix for staging tenants (default: `""`, staging auto-set to `staging-`).                                                        |

### How Secrets Are Deployed

The provisioner deploys all instance secrets as **`secret_text` bindings** directly on the User Worker. These are encrypted at rest by Cloudflare and available at runtime as plain `string` values (not Secrets Store objects).

The provisioner also creates an **empty Cloudflare Secrets Store** (`fouria-{slug}`) bound to the Worker for future dynamic secret use. Instance secrets are **never** placed in this store.

```elixir
# Per-tenant secrets (generated by lerma provisioner)
%{type: "secret_text", name: "MOLTBOT_GATEWAY_TOKEN", text: gateway_token}
%{type: "secret_text", name: "CDP_SECRET", text: value}
%{type: "secret_text", name: "CF_ACCESS_PLATFORM_AUD", text: platform_aud}
%{type: "secret_text", name: "CF_ACCESS_CLIENT_AUD", text: tenant_aud}  # Optional

# Platform-level secrets (from Lerma ENV, optional)
%{type: "secret_text", name: "CLOUDFLARE_AI_GATEWAY_API_KEY", text: ai_gateway_key}
%{type: "secret_text", name: "CLOUDFLARE_API_TOKEN", text: ai_gateway_key}  # Tenant REST alias
%{type: "secret_text", name: "STRIPE_SECRET_KEY", text: value}
# ... etc.
```

See `apps/lerma/lib/lerma/provisioning/tenant_provisioner.ex` for the authoritative binding list.

### Container-Backed Durable Objects

The tenant User Worker's `Sandbox` DO class is a container-backed Durable Object (it extends
`Container` from `@cloudflare/sandbox`). lerma deploys the worker into the dispatch namespace
with a `containers` array in the upload metadata (declaring `Sandbox`), which enables the
container runtime for that class — without it, Cloudflare throws
`Containers have not been enabled for this Durable Object class` when the worker calls into the
container (e.g. `/api/doctor`). lerma also creates/updates a container **application** for the
tenant (image + instance type + max instances) linked to the Sandbox DO namespace.

Container app image comes from `CONTAINER_IMAGE`, and the container instance type is derived
from the instance type chosen at provisioning time (defaults to `lite`, the cheapest tier).
`CONTAINER_MAX_INSTANCES` configures the max instances (see table above).

The tenant container application is pinned to the **WEUR** deployment zone by default
(`CONTAINER_REGIONS`) and container log capture is enabled (`CONTAINER_OBSERVABILITY_LOGS`).
Observability for the User Worker itself (logs + traces + invocation logs) mirrors the
`apps/fouria/wrangler.jsonc` block and is enabled by default
(`WORKER_OBSERVABILITY_*` vars above).

---

## Legacy Channel Secrets (E2E Testing Only)

> **Deprecated.** Channel secrets are now managed through the integrations dashboard (F4E1-encrypted R2 vault). These env vars are kept **only for E2E test fixtures**. Do not use in production.

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.

---

## Internal Variables (Auto-Set)

These are automatically derived by the Worker and should never be manually configured:

| Variable                              | Source                                                                                        |
| ------------------------------------- | --------------------------------------------------------------------------------------------- |
| `OPENCLAW_GATEWAY_TOKEN`              | Mapped from `MOLTBOT_GATEWAY_TOKEN`                                                           |
| `OPENCLAW_DEV_MODE`                   | Mapped from `DEV_MODE`                                                                        |
| `OPENCLAW_E2E_TEST_MODE`              | Mapped from `E2E_TEST_MODE`                                                                   |
| `CUSTOMER_SLUG`                       | Set from tenant resolution                                                                    |
| `OTEL_EXPORTER_OTLP_HEADERS`          | Auto: `Authorization=Bearer {OTEL_SHARED_SECRET}`                                             |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Auto: `{WORKER_URL}/api/otel/v1/metrics`                                                      |
| `MOLTLAZY_FEATURE_FLAGS`              | Set as per-customer binding (managed via FourIA API)                                          |
| `MOLTLAZY_ANALYTICS_DATASET`          | Auto-set to tenant Analytics Engine dataset                                                   |
| `CUSTOMER_ANALYTICS_DATASET`          | Auto-set to tenant Analytics Engine dataset                                                   |
| `STARTUP_LOG_VERBOSITY`               | Passed through from Worker env when set (`detailed` default in container)                     |
| `OPENCLAW_CONFIG_PATH`                | Baked into image: `/root/openclaw/.openclaw/openclaw.json` — the REAL config path (see below) |
| `OPENCLAW_STATE_DIR`                  | Baked into image: `/root/openclaw/.openclaw` — the REAL state dir (see below)                 |

> **Why `OPENCLAW_CONFIG_PATH` / `OPENCLAW_STATE_DIR` point at `/root/openclaw/.openclaw`:**
> `HOME=/home/openclaw` and `/home/openclaw/.openclaw` is a **symlink** to the real config dir at
> `/root/openclaw/.openclaw` (kept under the `/home/openclaw` snapshot root for R2 backups).
> OpenClaw 2026.8.2+ fs-safe refuses atomic file replaces when the immediate parent directory is a
> symlink ("Atomic replace parent must be a real directory"), so config/state writes through the
> `/home` symlink fail. These two env vars point OpenClaw at the real path for its own writes; the
> `/home/openclaw/.openclaw` symlink is preserved so backups and the Worker's `/home`-based config
> reads/writes keep working. Set in the Dockerfile (`apps/fouria/Dockerfile`), not per-tenant.

---

## Setting Variables

### Local Development (.dev.vars)

Copy from `.dev.vars.example`:

```bash
cp apps/fouria/.dev.vars.example apps/fouria/.dev.vars
# Edit .dev.vars with your values
# .dev.vars is gitignored — never commit it
```

In local dev, instance secrets are plain strings (simulating `secret_text` bindings). Dynamic secrets need `BACKUP_ENCRYPTION_KEY` configured with the F4E1 R2 store.

### Production Deployment

Instance secrets are deployed automatically by `lerma` when a tenant is provisioned. No manual `wrangler secret put` commands are needed.

Platform-level secrets should be set as environment variables in the dashboard's deployment:

```bash
STRIPE_SECRET_KEY=sk_live_...
OTEL_SHARED_SECRET=your-secret
# ... etc.
```

### OpenTofu (IaC) Mapping

> **Secrets reference:** See [docs/schemas/secrets-manifest.schema.json](./docs/schemas/secrets-manifest.schema.json) for the canonical credential inventory.
