FourIA — Environment Variables Reference
On this page
- Variable Classification
- Dynamic Secrets (F4E1-Encrypted R2 Storage)
- AI Provider Configuration
- Authentication & Access Control
- Persistence & Storage
- Cloudflare Tunnel
- Browser Automation (CDP)
- Container Rescue (/debug/rescue-container)
- Feature Flags
- Stripe Billing Integration
- Communication Schema (Dispatch Worker → User Worker)
- Dashboard Tenant Provisioning (apps/lerma)
- Legacy Channel Secrets (E2E Testing Only)
- Internal Variables (Auto-Set)
- Setting Variables
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 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.tsfor 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 readE2E_CLOUDFLARE_AI_GATEWAY_API_KEYand deploy it as theCLOUDFLARE_AI_GATEWAY_API_KEYruntime env. Referencing a non-existentsecrets.CLOUDFLARE_AI_GATEWAY_API_KEYexpands 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_TOKENis 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 toCLOUDFLARE_API_TOKEN(DEPLOY_WORKER_API_TOKENin CI).Secrets reference: See 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 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 for the canonical credential inventory.
Cloudflare Tunnel
Secrets reference: See 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 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 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 |
| 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 object as base64url JSON and sets it as the X-FourIA-Metadata header when forwarding the request.
BillingMetadata Schema
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 JWTaudis the platform AUD (the*.fouria.iowildcard Access application) or their email is a member of the sharedPaso4 Root UsersAccess 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 = validatedandclient_user_instanceslinks 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:
- JWT
audmatchesCF_ACCESS_PLATFORM_AUD→ROOT X-FourIA-Roleheader (from the Dispatch Worker)- JWT
custom.roleclaim (IdP-provided) 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.
# 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 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_DIRpoint at/root/openclaw/.openclaw:HOME=/home/openclawand/home/openclaw/.openclawis a symlink to the real config dir at/root/openclaw/.openclaw(kept under the/home/openclawsnapshot 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/homesymlink fail. These two env vars point OpenClaw at the real path for its own writes; the/home/openclaw/.openclawsymlink 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:
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:
STRIPE_SECRET_KEY=sk_live_...
OTEL_SHARED_SECRET=your-secret
# ... etc.OpenTofu (IaC) Mapping
Secrets reference: See docs/schemas/secrets-manifest.schema.json for the canonical credential inventory.