Message Ops does not use a single customer YAML configuration file. Infrastructure supplies process environment variables and secret references; the application stores environments, broker credentials, users, roles, sign-in policy, SSO providers, telemetry settings and retention settings in PostgreSQL through the UI/API.

Set `NODE_ENV=production` explicitly. Only `development` and `test` relax runtime hardening; an unset value defaults to development. Startup settings require a restart/redeployment. Saved application settings apply according to their reload or session rules. [Configuration Examples](https://portal.cjcit.io/knowledge-base/message_ops/1.7/configuration/examples/) shows the production origin and secret mapping.

## Required production secrets

<table><caption class="sr-only">Required production secrets</caption><thead><tr><th scope="col">Variable</th><th scope="col">Consumers</th><th scope="col">Requirement</th></tr></thead><tbody><tr><td><code>DATABASE_URL</code></td><td>API, Worker</td><td>Nonempty PostgreSQL URL using the restricted runtime login and certificate-verified TLS.</td></tr><tr><td><code>DATABASE_URL</code></td><td>Migration job</td><td>Separate privileged owner connection; never inject this into runtime apps.</td></tr><tr><td><code>SSO_SECRET_ENCRYPTION_KEY_BASE64</code></td><td>API, Worker</td><td>Each comma-separated encryption key decodes to exactly 32 bytes. Same ordered list on both services. Also protects telemetry credentials.</td></tr><tr><td><code>CREDENTIAL_SECRET_ENCRYPTION_KEY_BASE64</code></td><td>API, Worker</td><td>Exactly 32 bytes per key; same list on both. Protects broker passwords.</td></tr><tr><td><code>MFA_TOTP_SECRET_ENCRYPTION_KEY_BASE64</code></td><td>API</td><td>Exactly 32 bytes per encryption key.</td></tr><tr><td><code>COMPLIANCE_SIGNING_KEY_BASE64</code></td><td>API</td><td>Generate 32 bytes. Signing requires at least 32 decoded bytes; boot checks presence, not minimum signing length. Only the first entry signs and verifies.</td></tr><tr><td><code>DATABASE_SSL_CA_BASE64</code></td><td>Database clients, including operator jobs</td><td>Optional base64 PEM for a private database CA.</td></tr></tbody></table>

These secret variables accept a `<NAME>_FILE` alternative pointing to a readable UTF-8 secret mount. Direct and file forms together are an error. File contents are trimmed at both ends; empty database input fails outside development/test. API-only keys are not Worker requirements. Bootstrap/migration need the database credential and applicable database trust, not all API encryption keys.

Use versionless Key Vault references in Azure, with application and migration identities restricted to their own secrets. Encryption keys are not interchangeable with signing keys. Follow [Security and Secret Rotation](https://portal.cjcit.io/knowledge-base/message_ops/1.7/operations/security-and-secrets/) before changing any key.

## Network and authentication

<table><caption class="sr-only">Network and authentication</caption><thead><tr><th scope="col">Variable</th><th scope="col">Code default</th><th scope="col">Production use</th></tr></thead><tbody><tr><td><code>API_PORT</code> / <code>HOST</code></td><td><code>3000</code> / <code>0.0.0.0</code></td><td>Reference Azure API target is 3000.</td></tr><tr><td><code>API_UPSTREAM</code></td><td>Supplied by deployment</td><td>Web uses the internal API ingress FQDN without scheme or <code>:3000</code>.</td></tr><tr><td><code>AUTH_MODE</code></td><td><code>oidc</code></td><td>Keep <code>oidc</code>; <code>local</code> is development header authentication and is rejected in production. Password sign-in is controlled in Admin.</td></tr><tr><td><code>WEB_BASE_URL</code></td><td><code>http://localhost:5173</code></td><td>Set the public HTTPS browser origin.</td></tr><tr><td><code>CORS_ALLOWED_ORIGINS</code></td><td><code>http://localhost:5173</code></td><td>Explicit comma-separated allowed origins; do not leave the development fallback.</td></tr><tr><td><code>CSRF_PROTECTION_ENABLED</code></td><td><code>true</code></td><td>Keep enabled for cookie-authenticated changes.</td></tr><tr><td><code>SSO_CALLBACK_BASE_URL</code></td><td><code>http://localhost:3000/api/v1/auth/sso/callback</code></td><td>Set the full same-origin HTTPS callback registered at the IdP.</td></tr><tr><td><code>SSO_SESSION_STATE_TTL_SECONDS</code></td><td><code>600</code></td><td>Positive state/nonce/PKCE lifetime.</td></tr><tr><td><code>SSO_JIT_PROVISIONING_ENABLED</code></td><td><code>false</code></td><td>Enable only with deliberate provider and access policy.</td></tr><tr><td><code>MFA_TOTP_ISSUER</code> / <code>WEBAUTHN_RP_NAME</code></td><td>Particle Horizon | Message Operations</td><td>User-visible authenticator/passkey labels.</td></tr><tr><td><code>WEBAUTHN_RP_ID</code></td><td><code>localhost</code></td><td>Set the browser hostname.</td></tr><tr><td><code>WEBAUTHN_ORIGIN</code></td><td>Origin of <code>WEB_BASE_URL</code></td><td>Set the exact HTTPS origin when needed.</td></tr><tr><td><code>TRUST_PROXY</code></td><td><code>false</code></td><td>Reference Azure deployment sets <code>true</code>; keep the API internal.</td></tr><tr><td><code>TRUST_PROXY_HOPS</code></td><td><code>1</code></td><td>Reference Azure path sets <code>3</code>: Web ingress, Web nginx, API ingress.</td></tr><tr><td><code>TRUST_PROXY_CIDRS</code></td><td>Loopback and private-network ranges</td><td>Narrow to actual trusted proxy subnets. Invalid or empty entries prevent startup.</td></tr><tr><td><code>DATABASE_SSL_REJECT_UNAUTHORIZED</code></td><td><code>true</code></td><td>Keep certificate verification enabled.</td></tr><tr><td><code>ALLOW_INSECURE_DATABASE</code></td><td><code>false</code></td><td>Keep false for the supported TLS database deployment.</td></tr><tr><td><code>NODE_EXTRA_CA_CERTS</code></td><td>Unset</td><td>Optional mounted PEM extending Node HTTPS trust; set on both services when required.</td></tr><tr><td><code>RATE_LIMIT_WINDOW_MS</code> / <code>RATE_LIMIT_MAX</code></td><td><code>60000</code> / <code>300</code></td><td>Global HTTP throttle window and request budget.</td></tr></tbody></table>

The default trusted proxy ranges are `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `::1/128`, `fc00::/7` and `fe80::/10`. Client-IP resolution stops at the first untrusted hop or the hop limit. Validate the live path before relying on service-account IP allowlists.

Production database clients reject insecure URL transport settings and apply an explicit TLS policy. URL `ssl*` and `uselibpqcompat` options are removed before connection; use the database CA setting above rather than relying on a URL `sslrootcert` file. Do not disable verification to fix a missing CA.

`LOCAL_AUTH_ENABLED` is a compatibility setting and does not control current password sign-in. `OIDC_ISSUER_URL` and `OIDC_AUDIENCE` configure the legacy direct bearer validation path, not multi-provider SSO. Configure browser providers through Admin.

## Sessions and MFA

<table><caption class="sr-only">Sessions and MFA</caption><thead><tr><th scope="col">Variable</th><th scope="col">Default</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>MFA_PRE_AUTH_TTL_SECONDS</code></td><td><code>300</code></td><td>Pre-authentication challenge lifetime.</td></tr><tr><td><code>MFA_STEP_UP_TTL_SECONDS</code></td><td><code>600</code></td><td>Absolute recent-MFA window.</td></tr><tr><td><code>MFA_MAX_ATTEMPTS</code></td><td><code>5</code></td><td>MFA attempt budget.</td></tr><tr><td><code>LOGIN_MAX_ATTEMPTS</code></td><td><code>10</code></td><td>Password failure/in-flight budget; integer 1–1,000.</td></tr><tr><td><code>LOGIN_ATTEMPT_WINDOW_SECONDS</code></td><td><code>900</code></td><td>Fixed password budget window; integer 1–3,600.</td></tr><tr><td><code>SESSION_IDLE_TIMEOUT_SECONDS</code></td><td><code>900</code></td><td>Regular session idle limit.</td></tr><tr><td><code>SESSION_ABSOLUTE_TIMEOUT_SECONDS</code></td><td><code>7200</code></td><td>Regular session absolute limit.</td></tr><tr><td><code>ADMIN_SESSION_IDLE_TIMEOUT_SECONDS</code></td><td><code>600</code></td><td>Administrator idle limit.</td></tr><tr><td><code>ADMIN_SESSION_ABSOLUTE_TIMEOUT_SECONDS</code></td><td><code>3600</code></td><td>Administrator absolute limit.</td></tr><tr><td><code>SESSION_IDLE_WRITE_INTERVAL_SECONDS</code></td><td><code>60</code></td><td>Throttles persisted idle-deadline updates; sessions may expire slightly earlier.</td></tr></tbody></table>

## Broker and message controls

Configure endpoints, VPN and broker passwords through [Manage Environments](https://portal.cjcit.io/knowledge-base/message_ops/1.7/administration/environments/), not static broker environment variables. Set shared controls consistently on API and Worker.

<table><caption class="sr-only">Broker and message controls</caption><thead><tr><th scope="col">Variable</th><th scope="col">Default</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>SOLACE_ADAPTER</code></td><td><code>mock</code></td><td>Set <strong><code>real</code> on both API and Worker</strong> for production brokers.</td></tr><tr><td><code>SOLACE_CONNECT_TIMEOUT_MS</code></td><td><code>10000</code></td><td>Broker session connection timeout.</td></tr><tr><td><code>SOLACE_CONNECT_RETRIES</code></td><td><code>0</code></td><td>Broker connection retries.</td></tr><tr><td><code>SOLACE_BROWSE_TIMEOUT_MS</code></td><td><code>60000</code></td><td>Bounded queue browse time.</td></tr><tr><td><code>SOLACE_PUBLISH_ACK_TIMEOUT_MS</code></td><td><code>10000</code></td><td>Guaranteed publish acknowledgement wait; timeout is an uncertain outcome.</td></tr><tr><td><code>SOLACE_DISCONNECT_TIMEOUT_MS</code></td><td><code>5000</code></td><td>Bounded disconnect wait before disposal.</td></tr><tr><td><code>SOLACE_MAX_PAYLOAD_BYTES</code></td><td><code>5242880</code></td><td>Per-message payload size bound.</td></tr><tr><td><code>MESSAGE_SEARCH_DEFAULT_MAX_SCAN</code></td><td><code>100000</code></td><td>Default browse depth; hard ceiling 500,000.</td></tr><tr><td><code>MESSAGE_PAYLOAD_LOOKUP_MAX_SCAN</code></td><td><code>100000</code></td><td>Browse depth when finding a selected payload again.</td></tr><tr><td><code>MESSAGE_REF_TTL_SECONDS</code></td><td><code>3600</code></td><td>New reference lifetime; use the same value on API and Worker.</td></tr><tr><td><code>MAX_BULK_MESSAGES</code></td><td><code>500</code></td><td>Positive bulk selection bound, including criteria/schedules.</td></tr><tr><td><code>DISABLE_DELETE_OPERATIONS</code></td><td><code>false</code></td><td>Blocks deletes and moves.</td></tr><tr><td><code>DISABLE_MOVE_OPERATIONS</code></td><td><code>false</code></td><td>Blocks moves.</td></tr><tr><td><code>REQUIRE_CHANGE_REF_FOR_DELETE</code> / <code>REQUIRE_CHANGE_REF_FOR_MOVE</code></td><td>Parsed as <code>true</code></td><td>Ineffective compatibility entries in v1.7.1; changing them does not disable the production change-reference requirement.</td></tr><tr><td><code>PAYLOAD_MASKING_DISABLED_CATEGORIES</code></td><td>Empty</td><td>Comma-separated masking categories to disable; keep empty unless reviewed for your data.</td></tr></tbody></table>

With `NODE_ENV=production`, direct move/delete always require a change reference. Every production schedule template also requires one, including resubmission. No change-reference variable disables these production gates.

Deletion/move containment must be configured on **both** services and followed by restart. Terraform uses shared `disable_delete_operations` and `disable_move_operations` inputs; per-component entries for these switches are rejected. Resubmission is unaffected by these two switches. An environment’s read-only setting blocks all three mutations. Already executing broker work is not rolled back. Queued attempts and later items recheck controls; failed jobs need a new submission after remediation. Schedule disable/delete remains possible.

## Worker and queue

<table><caption class="sr-only">Worker and queue</caption><thead><tr><th scope="col">Variable</th><th scope="col">Default</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>JOB_QUEUE_NAME</code></td><td><code>jobs</code></td><td>Must agree across API, Worker and migrator; exhausted delivery queue is <code>&lt;name&gt;-dead</code>.</td></tr><tr><td><code>JOB_MAX_ATTEMPTS</code></td><td><code>3</code></td><td>Total delivery attempts including the first.</td></tr><tr><td><code>JOB_BACKOFF_DELAY_MS</code></td><td><code>5000</code></td><td>Base exponential retry delay.</td></tr><tr><td><code>JOB_EXPIRE_SECONDS</code></td><td><code>900</code></td><td>Stalled-job recovery interval.</td></tr><tr><td><code>WORKER_CONCURRENCY</code></td><td><code>4</code></td><td>Concurrent jobs per Worker replica.</td></tr><tr><td><code>POLLING_INTERVAL_SECONDS</code></td><td><code>2</code></td><td>Worker poll interval.</td></tr><tr><td><code>QUEUE_BATCH_SIZE</code></td><td><code>1</code></td><td>Fetch batch size.</td></tr><tr><td><code>SHUTDOWN_TIMEOUT_MS</code></td><td><code>30000</code></td><td>API/Worker job-queue graceful shutdown wait.</td></tr><tr><td><code>WORKER_METRICS_PORT</code></td><td><code>9090</code></td><td>Worker health port; it is not a Prometheus metrics endpoint.</td></tr><tr><td><code>SCHEDULE_TICK_ENABLED</code></td><td><code>true</code></td><td>Enables Worker schedule firing.</td></tr><tr><td><code>SCHEDULE_TICK_INTERVAL_MS</code></td><td><code>60000</code></td><td>Schedule due-check cadence.</td></tr><tr><td><code>RUN_MIGRATIONS_ON_BOOT</code></td><td>Deployment pins <code>false</code></td><td>Compatibility marker; runtime never performs migrations. Use the dedicated job.</td></tr></tbody></table>

Changing queue settings does not create missing queues: apply the matching dedicated migration configuration first. Never use the in-memory test queue driver in production. Job delivery retries do not imply that an uncertain publication is safe to repeat.

## Background polling, retention and telemetry

<table><caption class="sr-only">Background polling, retention and telemetry</caption><thead><tr><th scope="col">Variable</th><th scope="col">Default</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>ENVIRONMENT_HEARTBEAT_ENABLED</code></td><td><code>true</code></td><td>Fallback heartbeat enablement.</td></tr><tr><td><code>ENVIRONMENT_HEARTBEAT_INTERVAL_MS</code></td><td><code>30000</code></td><td>Fallback heartbeat interval.</td></tr><tr><td><code>ENVIRONMENT_HEARTBEAT_RETRIES</code></td><td><code>0</code></td><td>Heartbeat retries.</td></tr><tr><td><code>QUEUE_INVENTORY_REFRESH_ENABLED</code></td><td><code>true</code></td><td>Fallback scheduled inventory enablement.</td></tr><tr><td><code>QUEUE_INVENTORY_REFRESH_INTERVAL_MS</code></td><td><code>86400000</code></td><td>Fallback inventory interval.</td></tr><tr><td><code>MESSAGE_REFS_RETENTION_DAYS</code></td><td><code>30</code></td><td>Reference age cap, independent of earlier per-row expiry.</td></tr><tr><td><code>AUDIT_EVENTS_RETENTION_DAYS</code></td><td><code>365</code></td><td>Audit fallback; 0 retains indefinitely. Finite values below 90 are clamped.</td></tr><tr><td><code>COMPLIANCE_EVIDENCE_RETENTION_DAYS</code></td><td><code>90</code></td><td>Expiry assigned to new evidence.</td></tr><tr><td><code>RETENTION_SCHEDULER_ENABLED</code></td><td><code>true</code></td><td>API retention maintenance; automatically disabled in tests.</td></tr><tr><td><code>COMPLIANCE_SCHEDULER_ENABLED</code></td><td><code>true</code></td><td>API weekly evidence generation/daily purge; disabled in tests.</td></tr><tr><td><code>OTEL_TRACES_SAMPLER_RATIO</code></td><td><code>1</code></td><td>Startup trace sampling; saved settings replace it.</td></tr><tr><td><code>OBSERVABILITY_RELOAD_INTERVAL_MS</code></td><td><code>30000</code></td><td>Saved telemetry reload; 0 disables automatic polling.</td></tr></tbody></table>

[SEMP Traffic](https://portal.cjcit.io/knowledge-base/message_ops/1.7/administration/semp-traffic/) overrides its deployment fallbacks after settings are saved. **Admin → Settings** overrides retention fallbacks; evidence changes apply to new bundles. [Monitoring and Logging](https://portal.cjcit.io/knowledge-base/message_ops/1.7/operations/observability/) describes saved telemetry destinations and credentials.

Development compatibility and test-only switches are not a production configuration method. In particular, API and Worker refuse `DISABLE_LICENSE_CHECK=true` outside development/test.
