Skip to main content

Configuration reference

This is the complete reference of what Converger reads from the environment and from config/*.exs. It was compiled from every System.get_env / System.fetch_env! call in config/ and lib/ and from every Application.get_env key. For a deployment-oriented walkthrough (which variables to set first, TLS behind a proxy, compose secrets) see Deployment.

How configuration is loaded​

FileWhen it is evaluatedWhat belongs there
config/config.exsAt build time (mix compile, mix release)Defaults for every environment: Oban, pipeline backend, uploads, rate limits, pagination, OpenTelemetry resource
config/dev.exs, config/test.exs, config/prod.exsAt build time, imported at the end of config.exsPer-environment overrides (dev database, test sandbox, prod JSON logging)
config/runtime.exsAt boot of every environment, including releasesEverything read from environment variables

Because releases evaluate config/runtime.exs on every boot, changing an environment variable only needs a restart, never a rebuild. Almost all application code reads its settings with Application.get_env/3 at call time, so keys from config.exs can also be overridden in runtime.exs. The single compile-time key is :dev_routes (read with Application.compile_env/2 in the router).

Conventions used in the tables below:

  • Required (prod): the release refuses to boot without it.
  • Boolean variables are parsed differently depending on the variable; the accepted "true" spellings are listed per variable. Anything else counts as false.
  • Comma-separated lists are split on ,, trimmed, and empty entries are dropped.

Required in production​

VariableDefaultMeaning
DATABASE_URLnonePostgres URL, ecto://USER:PASS@HOST/DATABASE. Prod only.
SECRET_KEY_BASEnoneSigns and encrypts cookies, LiveView sessions and every JWT (Converger.Auth.Signer). At least 64 bytes; generate with mix phx.gen.secret. Values once published in the repository are rejected by SHA-256 fingerprint. Prod only (dev and test use fixed values from dev.exs / test.exs).
CLOAK_KEYnoneBase64-encoded 32-byte AES key for Converger.Vault (channel secrets and configs at rest). Generate with openssl rand -base64 32. The published demo key is rejected. Prod only; dev and test use fixed keys. See Security model.

HTTP server and endpoint​

VariableDefaultRequired (prod)Meaning
PHX_SERVERunsetyes, for releasesWhen set to any value (even false), starts the HTTP endpoint. bin/server sets PHX_SERVER=true. Read in every environment.
PHX_HOSTexample.comeffectively yesPublic host name. URLs are generated as https://PHX_HOST:443, ForceSSL redirects to it, and the default WebSocket origin check accepts only this host. Prod only.
PORT4000noHTTP listen port. Prod binds all interfaces (::); dev binds 127.0.0.1. Read in prod and dev. The port speaks HTTP/1.1 only (REST, SSE, WebSocket): HTTP/2 is disabled in config/config.exs. gRPC has its own port (gRPC transport).
CHECK_ORIGINhost of PHX_HOSTnoComma-separated origins allowed to open browser WebSockets (LiveView, /socket, /socket/converger), Phoenix syntax such as //*.example.com. Set but empty raises at boot. Prod only.
CORS_ORIGINShttp://127.0.0.1:5500,http://localhost:5500noComma-separated CORS origins (* for any). Read per request by ConvergerWeb.Endpoint.cors_origins/0. All environments.

TLS and HSTS​

All prod only. Boolean values accept true, 1, yes, on (case-insensitive). See TLS, HSTS and WebSocket origins.

VariableDefaultMeaning
FORCE_SSLtrueEnables ConvergerWeb.Plugs.ForceSSL: redirect plain HTTP to https://PHX_HOST, HSTS on HTTPS responses. X-Forwarded-Proto is trusted only from TRUSTED_PROXIES.
HSTStrueSend strict-transport-security.
HSTS_MAX_AGE31536000HSTS max-age in seconds.
HSTS_INCLUDE_SUBDOMAINSfalseAdd includeSubDomains.
HSTS_PRELOADfalseAdd preload.
FORCE_SSL_EXCLUDE_PATHSemptyComma-separated paths served over plain HTTP without redirect. Hosts localhost and 127.0.0.1 are always excluded.

Database​

VariableDefaultEnvironmentMeaning
DATABASE_URLnoneprodSee Required in production.
POOL_SIZE10prodInteractive pool size per node (HTTP, WebSocket, LiveView). Dev uses 20, test 2 x schedulers. See Database pools and pressure.
DB_QUEUE_TARGET_MS50prodInteractive pool queue_target: a checkout queued longer than this for a whole DB_QUEUE_INTERVAL_MS is dropped and the request answered 503.
DB_QUEUE_INTERVAL_MS1000prodInteractive pool queue_interval.
DB_TIMEOUT_MS15000prodInteractive query timeout.
BACKGROUND_POOL_SIZE10prodBackground pool size per node, used by Oban jobs. 0 shares the interactive pool (one pool, as before #35). Dev uses 10, test shares.
BACKGROUND_DB_QUEUE_TARGET_MS500prodBackground pool queue_target.
BACKGROUND_DB_QUEUE_INTERVAL_MS5000prodBackground pool queue_interval.
BACKGROUND_DB_TIMEOUT_MS60000prodBackground query timeout.
DB_RETRY_AFTER_MS1000allBase of the Retry-After / retryAfterMs hint when the database is unavailable; jittered up to twice the value.
READY_DB_QUEUE_MS200all/health/ready reports degraded (503) while the interactive pool's average wait is at least this long. 0 disables.
DB_QUEUE_WINDOW_MS5000allWindow of that average.
ECTO_IPV6unsetprodtrue or 1 connects over IPv6 (socket_options: [:inet6]).
DB_USERNAMEpostgresdev, testPostgres user.
DB_PASSWORDpostgresdev, testPostgres password.
DB_HOSTNAMElocalhostdev, testPostgres host.
DB_PORT5432testPostgres port (used by the 100M-row partition run, which starts its own Postgres on 15432).
DB_NAMEconverger_dev / converger_test + MIX_TEST_PARTITIONdev, testDatabase name.
MIX_TEST_PARTITIONunsettestSuffix for the test database and the test upload directory, so several suites can run on one machine.

Secrets at rest​

VariableDefaultEnvironmentMeaning
CLOAK_KEYnoneprod (required)Current encryption key.
CLOAK_RETIRED_KEYSemptyprodComma-separated base64 keys that are still accepted for decryption during a rotation. Run bin/converger eval "Converger.Release.reencrypt_secrets()" after rotating, then remove them.

Client tokens​

Converger client tokens (JWT): signing keys, refresh rotation and revocation. See Client tokens and ADR-0059.

VariableDefaultEnvironmentMeaning
TOKEN_SIGNING_KEYSunset: SECRET_KEY_BASE under the kid defaultallComma-separated kid:secret pairs, the signing key first; every listed key verifies. Secrets must be at least 32 bytes and kids unique, or the node refuses to boot. Rotate by prepending a new key and removing the old one after 30 minutes.
TOKEN_ROTATION_GRACE_SECONDS30allHow long a refreshed token stays valid. Refreshing it again after that revokes its family (reuse detection).
TOKEN_ACCEPT_UNKEYEDtrueallAccept Converger tokens without a kid header (issued before #52, signed with SECRET_KEY_BASE). Set to false 30 minutes after upgrading.

Password hashing​

VariableDefaultRead atMeaning
CONVERGER_PASSWORD_HASHERbcryptcompile time (mix.exs, config/config.exs)bcrypt (bcrypt_elixir, a C NIF) or pbkdf2 (pbkdf2_elixir, pure Elixir). pbkdf2 leaves bcrypt_elixir out of the build, for development machines without a C compiler. Existing hashes are verified by their own prefix; bcrypt hashes cannot be verified in a pbkdf2 build. Release images use the default. See ADR-0057.

Network trust and admin access​

VariableDefaultEnvironmentMeaning
TRUSTED_PROXIESempty (forwarding headers ignored)allComma-separated IPs/CIDRs whose X-Forwarded-For / X-Forwarded-Proto are honoured. See ../security.md.
ADMIN_IP_WHITELIST127.0.0.1,::1allComma-separated IPs/CIDRs allowed to reach /admin and the admin API /api/v1/admin.
ADMIN_API_TOKENunset (admin API off)allBearer token of the platform admin API (/api/v1/admin): at least 32 characters (openssl rand -base64 48); shorter values stop the boot. Unset, the admin API answers 401.

Accounts, sessions and email​

Admin panel and tenant portal logins, 2FA and password reset (Security model, ADR-0058).

VariableDefaultEnvironmentMeaning
SESSION_MAX_AGE_SECONDS43200 (12 h)allAbsolute lifetime of an admin/portal session from login, whatever the activity. Also the session cookie's max_age.
SESSION_IDLE_TIMEOUT_SECONDS1800 (30 min)allA session without requests for this long ends.
PASSWORD_MIN_LENGTH12allMinimum password length for admins and tenant users, applied when a password is set (existing passwords keep working). The maximum is fixed at 72 bytes (bcrypt).
ADMIN_REQUIRE_2FA_ROLESsuper_adminallComma-separated admin roles (super_admin, admin, viewer) that must enable TOTP 2FA before using the admin panel. Empty ("") requires it for nobody.
MAILER_FROMConverger <[email protected]>allSender of account emails (password reset): [email protected] or Name <[email protected]>.
SMTP_HOSTunsetallWhen set, Converger.Mailer sends through this SMTP relay. Without it production sends no email, so password reset links never arrive.
SMTP_PORT587allSMTP port.
SMTP_USERNAME / SMTP_PASSWORDunsetallSMTP credentials; authentication is used when SMTP_USERNAME is set.
SMTP_SSLfalsealltrue or 1 for implicit TLS (usually port 465). Otherwise STARTTLS is required.

Reset links are built from the endpoint URL (PHX_HOST, see HTTP server and endpoint), so it must be the address users open.

Outbound webhooks (SSRF guard)​

VariableDefaultEnvironmentMeaning
WEBHOOK_ALLOWED_TARGETSempty (dev: localhost,127.0.0.0/8,::1 from dev.exs)allComma-separated host names (*.svc.local matches subdomains) or IP/CIDR ranges that webhooks may target although they are private. Replaces the dev default when set.
WEBHOOK_ALLOW_PRIVATE_TARGETSunsetallExactly true or 1 disables the SSRF guard entirely. Development only.

See Outbound webhooks. The same guard, and these two variables, apply to platform event webhook subscriptions and to the broker hosts of broker channels (kafka brokers, the rabbitmq and redis_streams URL hosts, the nats servers, the mqtt host): allow a private broker with WEBHOOK_ALLOWED_TARGETS.

Platform event webhooks​

config :converger, :platform_webhooks,
enabled: true, # false stops webhook deliveries (events still reach the backbone)
connect_timeout: 5_000, # ms
receive_timeout: 10_000, # ms
max_response_bytes: 65_536

Retries use the shared :retry_policy; the event log is pruned after WEBHOOK_EVENT_RETENTION_DAYS. See Platform event webhooks.

Broker channels​

Consumers of the kafka, rabbitmq, nats, redis_streams and mqtt channel types, and the Gateway connections of discord channels with gateway: true (Broker channels, Discord Gateway mode).

VariableDefaultEnvironmentMeaning
BROKER_CONSUMERStrue (test: false)allfalse / 0 keeps broker consumers off this node (no consumer manager); the other nodes of the cluster run them. Producing (deliveries to broker channels) is not affected.
BROKER_RECONCILE_INTERVAL_MS30000allHow often every node reconciles the running consumers with the channels (restarts consumers that gave up, picks up the consumers of a node that left). Channel changes are applied at once regardless.

The other keys of config :converger, :broker_channels (application config only):

KeyDefaultMeaning
consumerstrueSet by BROKER_CONSUMERS.
reconcile_interval_ms30000Set by BROKER_RECONCILE_INTERVAL_MS.
inbound_max_attempts8Attempts for a consumed message that fails transiently before it is dead-lettered.
inbound_backoff_ms, inbound_max_backoff_ms500, 30000Backoff between those attempts (doubling, capped).
dead_letter_max_backoff_ms30000Cap of the backoff between attempts to write a message to the dead-letter topic or queue (retried until it succeeds).
publisher_idle_ms900000A node's producer connection to a broker channel is closed after this long without deliveries.

Event backbone​

Where platform events are published and consumed (Event backbone, ADR-0070). How to choose one and migrate without downtime: Deployment. All variables apply in every environment; unset or empty ones keep the config.exs defaults.

VariableDefaultMeaning
BACKBONEnativeThe backbone: native, kafka, nats or rabbitmq. Any other value fails the boot.
BACKBONE_MIRRORunsetComma-separated backbones that also receive every event (dual publish while migrating).
BACKBONE_CONSUMERStruefalse / 0: this node declares the subscriptions but runs no broker consumer. Publishing is not affected.
BACKBONE_TOPIC_PREFIXconvergerKafka topics and NATS subjects <prefix>.activities, .deliveries, .events; NATS streams <PREFIX>_ACTIVITIES ...; the RabbitMQ exchange.
BACKBONE_KAFKA_BROKERSlocalhost:9092Bootstrap brokers, host:port,host:port.
BACKBONE_KAFKA_SASL_MECHANISMunsetplain, scram_sha_256 or scram_sha_512, with BACKBONE_KAFKA_SASL_USERNAME / _PASSWORD.
BACKBONE_KAFKA_TLSfalseConnect with TLS (verifying the broker certificate against the OS trust store).
BACKBONE_KAFKA_PARTITIONS12Partitions of topics created at boot. More partitions, more consumer parallelism.
BACKBONE_KAFKA_REPLICATION_FACTOR1Replication factor of topics created at boot; use 3 on a production cluster.
BACKBONE_KAFKA_START_OFFSETearliestWhere a new consumer group starts: earliest or latest.
BACKBONE_NATS_SERVERSnats://localhost:4222Servers, nats://host:port or tls://host:port, comma-separated.
BACKBONE_NATS_USERNAME / _PASSWORD / _TOKENunsetNATS credentials.
BACKBONE_NATS_TLSfalseConnect with TLS.
BACKBONE_NATS_REPLICAS1Replicas of streams created at boot.
BACKBONE_RABBITMQ_URLamqp://localhost:5672amqp:// or amqps:// URL (host, port, virtual host).
BACKBONE_RABBITMQ_USERNAME / _PASSWORDunsetRabbitMQ credentials.

Broker hosts come from the operator, so the webhook SSRF guard does not apply to them.

Application config:

config :converger, :backbone, Converger.Backbone.Native # .Kafka | .Nats | .RabbitMQ

config :converger, Converger.Backbone,
topic_prefix: "converger",
mirror: [], # e.g. [Converger.Backbone.Kafka] while migrating
consumers: true,
subscriptions: [], # extra subscriptions: [{name, topics, handler, opts}]
inline_attempts: 3, # a failing handler on a broker consumer, then parked in Oban
inline_backoff_ms: 200,
down_ms: 5_000 # after a failed publish, park at once for this long

config :converger, Converger.Backbone.Kafka,
brokers: "localhost:9092", sasl_mechanism: nil, sasl_username: nil, sasl_password: nil,
tls: false, tls_verify: true, tls_ca_cert: nil,
create_topics: true, partitions: 12, replication_factor: 1,
start_offset: "earliest", produce_timeout_ms: 10_000,
producer_concurrency: 2, # consumer group members per node and subscription
fetch_config: [max_wait_time: 10], # BroadwayKafka fetch options (ms an empty partition is waited for)
receive_interval: 100, # ms before a drained producer polls again
group_config: [] # BroadwayKafka group options; default session_timeout_seconds: 10, heartbeat_rate_seconds: 2

config :converger, Converger.Backbone.Nats,
servers: "nats://localhost:4222", username: nil, password: nil, token: nil, creds: nil, nkey_seed: nil,
tls: false, tls_verify: true, tls_ca_cert: nil,
replicas: 1, max_age_seconds: 604_800, duplicate_window_seconds: 120,
ack_wait_seconds: 30, batch_size: 10, publish_timeout_ms: 10_000

config :converger, Converger.Backbone.RabbitMQ,
url: "amqp://localhost:5672", username: nil, password: nil, tls_verify: true, tls_ca_cert: nil,
prefetch_count: 20, publish_timeout_ms: 10_000

Idempotency receipts are pruned after BACKBONE_RECEIPT_RETENTION_DAYS (7, backbone_receipt_days of config :converger, Converger.Retention).

Pagination​

All environments. Values are integers; unset or empty variables keep the config.exs default. See ADR-0018.

VariableConfig keyDefaultMeaning
PAGINATION_DEFAULT_LIMITdefault_limit50Page size for conversations, audit logs, deliveries, tenant users when limit is omitted or invalid.
PAGINATION_MAX_LIMITmax_limit500Upper clamp for a requested limit on those lists.
PAGINATION_ACTIVITY_DEFAULT_LIMITactivity_default_limit100Default page size for activities (REST and transcript views).
PAGINATION_ACTIVITY_MAX_LIMITactivity_max_limit1000Upper clamp for activity pages.
PAGINATION_WS_REPLAY_LIMITws_replay_limit100Maximum activities replayed on WebSocket join; the rest come over REST.
PAGINATION_LOOKUP_LIMITlookup_limit1000Hard cap for small operator tables listed whole (tenants, channels, routing rules, admin users).

Rate limiting​

VariableDefaultEnvironmentMeaning
RATE_LIMIT_BACKENDcluster when CLUSTER_STRATEGY (or DNS_CLUSTER_QUERY) enables clustering, otherwise localall except testlocal or cluster; any other value raises at boot.
RATE_LIMIT_SYNC_INTERVAL_MS100allHow often the cluster backend broadcasts counter deltas.

Limits themselves are not environment variables; see Rate limiting.

WebSocket limits and draining​

All environments. Integers; unset or empty variables keep the config :converger, :websocket default. See WebSocket limits and draining for what each one does.

VariableConfig keyDefault
WS_MAX_FRAME_BYTESmax_frame_bytes131072
WS_MAX_MESSAGES_PER_WINDOWmax_messages20
WS_RATE_WINDOW_MSrate_window_ms1000
WS_MAX_JOINSmax_joins50
WS_MAX_IN_FLIGHTmax_in_flight32
WS_EPHEMERAL_DROP_QUEUE_LENephemeral_drop_queue_len100
WS_SLOW_CONSUMER_QUEUE_LENslow_consumer_queue_len1000
WS_RECONNECT_BASE_MSreconnect_base_ms1000
WS_RECONNECT_JITTER_MSreconnect_jitter_ms5000
WS_DRAIN_DELAY_MSdrain_delay_ms5000 (0 in test)
WS_DRAIN_BATCH_SIZEdrain_batch_size500
WS_DRAIN_BATCH_INTERVAL_MSdrain_batch_interval_ms1000
WS_DRAIN_SHUTDOWN_MSdrain_shutdown_ms30000

The hard frame cap, config :converger, :websocket_max_frame_size (1_048_576), is compile time.

gRPC transport​

All environments. Protocol v1 over a gRPC bidirectional stream (Protocol v1 over gRPC, config :converger, ConvergerWeb.GRPC). The WebSocket limits above apply to its streams too.

VariableConfig keyDefaultDescription
GRPC_ENABLEDenabledfalsetrue or 1 starts the gRPC server.
GRPC_PORTport50051Listening port (all interfaces, IPv4 and IPv6).
GRPC_TLS_CERTFILEcertfileunsetPEM certificate path. With GRPC_TLS_KEYFILE the server speaks TLS; otherwise plaintext HTTP/2 (h2c).
GRPC_TLS_KEYFILEkeyfileunsetPEM private key path.

Clustering​

Read in every environment except test (see Clustering). The Erlang distribution itself (node name, cookie) is configured by rel/env.sh.eex when the release starts.

VariableDefaultEnvironmentMeaning
CLUSTER_STRATEGYnone (dns when DNS_CLUSTER_QUERY is set)all except testHow nodes find each other: none, kubernetes_dns, dns, gossip or epmd. Any other value raises at boot. Anything but none also switches the default rate-limit backend to cluster.
CLUSTER_NODE_BASENAMEconvergerall except testName part of every node (<basename>@<ip>). Used by rel/env.sh.eex for RELEASE_NODE and by the kubernetes_dns / dns strategies to build node names.
CLUSTER_SERVICEnonekubernetes_dnsHeadless Service name, e.g. converger-headless or converger-headless.<namespace>.svc.cluster.local. Required; missing raises at boot.
CLUSTER_DNS_QUERYDNS_CLUSTER_QUERYdnsDNS name whose A/AAAA records are the node IPs (e.g. myapp.internal on Fly.io). Required.
CLUSTER_POLL_INTERVAL_MS5000kubernetes_dns, dnsHow often DNS is polled for new or removed nodes.
CLUSTER_GOSSIP_SECRETunsetgossipEncrypts and authenticates the gossip packets. Set it; without it any host on the network can announce nodes (connecting still needs the cookie).
CLUSTER_GOSSIP_PORT, CLUSTER_GOSSIP_IF_ADDR, CLUSTER_GOSSIP_MULTICAST_ADDR, CLUSTER_GOSSIP_MULTICAST_IFlibcluster defaults (45892, 0.0.0.0, 233.252.1.32)gossipUDP multicast settings.
CLUSTER_GOSSIP_BROADCAST_ONLYfalsegossiptrue/1: use broadcast instead of multicast (set CLUSTER_GOSSIP_MULTICAST_ADDR to the broadcast address).
CLUSTER_HOSTSemptyepmdComma-separated node names to connect to (a@host1,b@host2). Empty: every node registered in the local EPMD.
DNS_CLUSTER_QUERYunsetall except testDeprecated alias from the former dns_cluster dependency: implies CLUSTER_STRATEGY=dns with this query.
RELEASE_COOKIEnone when clusteringreleasesShared secret of the Erlang distribution. Required when CLUSTER_STRATEGY is not none: bin/converger start refuses to boot without it (the cookie generated at build time is baked into the image).
RELEASE_DISTRIBUTIONname when clustering, else snamereleasesStandard Mix release variable; long names are needed for name@ip.
RELEASE_NODE<CLUSTER_NODE_BASENAME>@<IP> when clusteringreleasesStandard Mix release variable. The IP is POD_IP, then FLY_PRIVATE_IP, then hostname -i. An IPv6 address switches the distribution to inet6_tcp.
POD_IP, FLY_PRIVATE_IPunsetreleasesNode address used for RELEASE_NODE (Kubernetes downward API, Fly.io).
CLUSTER_DIST_PORTunset (random port)releasesFixed Erlang distribution port of the server, for firewalls and NetworkPolicies. Applied by rel/env.sh.eex to start/daemon only, so bin/converger rpc and remote can still run in the same pod. The Helm chart sets 9100.

Metrics and health​

VariableDefaultEnvironmentMeaning
METRICS_TOKENunsetallBearer token for GET /metrics on the main port (Authorization: Bearer <token>). See Observability.
METRICS_ALLOWED_IPSunset (dev: loopback and private ranges)allComma-separated IPs/CIDRs allowed to read /metrics without a token (client IP after TRUSTED_PROXIES). With neither variable set, /metrics answers 404.
PROMETHEUS_PORTunsetallStarts the old separate, unauthenticated metrics listener on this port (/metrics). Only for scrapers that cannot send a token; never expose it publicly.
METRICS_TENANT_LABELfalsealltrue (or 1) adds a tenant_id label to the delivery, pipeline-lag and inbound series. One series per tenant and channel type: leave it off with many tenants. See Observability.
METRICS_POLL_INTERVAL_MS15000allHow often every node reads the database-backed gauges (Oban queue depth, dead letters, open circuit breakers).

GET /health/live and GET /health/ready need no configuration. How long a stopping node keeps serving after readiness turns draining is WS_DRAIN_DELAY_MS (see WebSocket limits and draining).

OpenTelemetry tracing​

config/runtime.exs enables the OTLP exporter only when one of the two endpoint variables is non-blank, and never in test. The other variables are read by opentelemetry_exporter and the opentelemetry SDK themselves. See Observability.

VariableDefaultRead byMeaning
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTunsetruntime.exs, exporterFull traces URL. Takes precedence; enables export.
OTEL_EXPORTER_OTLP_ENDPOINTunsetruntime.exs, exporterCollector base URL (/v1/traces is appended). Enables export.
OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOLhttp_protobufexporterhttp_protobuf or grpc.
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERSunsetexporterkey=value,key2=value2 headers for the collector.
OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_COMPRESSIONnoneexportergzip to compress.
OTEL_SERVICE_NAMEconverger (from config.exs)SDKService name on every span.
OTEL_RESOURCE_ATTRIBUTESunsetSDKExtra resource attributes.
OTEL_TRACES_EXPORTERderivedSDKnone forces export off.
OTEL_SDK_DISABLEDfalseSDKtrue disables the SDK.

File storage and CDN​

Applied only when UPLOAD_STORAGE is set, and never in test. Otherwise the config.exs defaults (local disk in priv/uploads, 10 MB) stay in effect. Backend details: File storage and attachments.

VariableDefaultRequiredMeaning
UPLOAD_STORAGEunsetnolocal, s3, minio, r2, gcs or azure; anything else raises.
UPLOAD_DIRpriv/uploadsnoDirectory for local. Use a persistent volume in releases.
UPLOAD_MAX_BYTES10485760noMaximum upload size.
UPLOAD_SIGNED_URL_TTL300noLifetime of signed download URLs, seconds.
UPLOAD_ALLOWED_TYPESbuilt-in listnoComma-separated MIME allowlist (tenants can override with allowed_upload_types).
S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEYnoneyes for s3/minio/r2Bucket and credentials (System.fetch_env!).
S3_SESSION_TOKENunsetnoSTS session token.
S3_REGIONunset (backend default us-east-1)noRegion; auto for R2.
S3_ENDPOINTunset (AWS endpoint)noCustom endpoint for MinIO / R2.
S3_PATH_STYLEfalse for s3, true for minio/r2notrue, 1 or yes for path-style URLs.
GCS_BUCKET, GCS_HMAC_ACCESS_ID, GCS_HMAC_SECRETnoneyes for gcsBucket and HMAC key.
GCS_ENDPOINTunset (https://storage.googleapis.com)noCustom endpoint.
AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, AZURE_STORAGE_CONTAINERnoneyes for azureAccount, base64 account key, container.
AZURE_STORAGE_ENDPOINTunsetnoCustom endpoint (Azurite).
CDN_TYPEunset (no CDN)nocloudfront, google_cdn or plain; anything else raises.
CDN_BASE_URLnoneyes when CDN_TYPE is setCDN base URL.
CDN_PATH_PREFIXemptynoPrefix prepended to the storage key in CDN URLs (<base_url>/<path_prefix>/<storage_key>).
CLOUDFRONT_KEY_PAIR_IDnoneyes for cloudfrontKey pair id.
CLOUDFRONT_PRIVATE_KEY / CLOUDFRONT_PRIVATE_KEY_FILEnoneone of them for cloudfrontPEM private key inline, or a path to it.
GOOGLE_CDN_KEY_NAME, GOOGLE_CDN_KEYnoneyes for google_cdnSigned URL key name and key.
CDN_SIGN_ORIGINunsetnoplain CDN only: true, 1 or yes appends the backend's signed query string (for example an Azure SAS) to the CDN URL.

Data retention and archive​

See Data retention, partitions and archive (issue #30). The ARCHIVE_BUCKET / ARCHIVE_CONTAINER / ARCHIVE_DIR overrides are read together with UPLOAD_STORAGE (only when it is set); without them the archive uses the attachment storage.

VariableDefaultRequiredMeaning
RETENTION_MIN_DAYS30noPlatform minimum for tenants.retention_days; months that ended less than this many days ago are never archived.
HEALTH_CHECK_RETENTION_DAYS7noDelete channel_health_checks older than this (daily); 0 disables.
AUDIT_LOG_RETENTION_DAYS365noDelete audit_logs older than this (daily); 0 disables.
WEBHOOK_EVENT_RETENTION_DAYS7noDelete platform webhook webhook_events older than this (daily); 0 disables.
PARTITION_MONTHS_AHEAD3noMonthly partitions of activities / deliveries kept in place ahead of the current month.
ARCHIVE_BUCKETunset (attachment bucket)noS3, MinIO, R2 or GCS bucket for the archive, with the UPLOAD_STORAGE credentials.
ARCHIVE_CONTAINERunset (attachment container)noAzure container for the archive.
ARCHIVE_DIRunset (attachment directory)noDirectory for the archive with UPLOAD_STORAGE=local.
ARCHIVE_PREFIXarchivenoFirst segment of archive object keys.
ARCHIVE_PART_ROWS50000noRows per archive object.
PARTITION_MAX_INLINE_ROWS1000000noRead by the partitioning migration (bin/migrate): above this many activities it refuses to copy inline and asks for the online Converger.Release.prepare_partitioning/0 first. See Migrations.

Release scripts and seeding​

VariableDefaultRead byMeaning
CREATE_DBfalserel/overlays/bin/migrateExactly true runs Converger.Release.create_db/0 before migrating.
ADMIN_EMAIL[email protected]Converger.Release.seed_admin/0, priv/repo/seeds.exsEmail of the first super_admin.
ADMIN_PASSWORDgeneratedsamePassword of that account. When unset, a random one is printed once and must_change_password is set.

Converger.Release.reset_admin_2fa/1 turns off 2FA of a locked-out admin by email: bin/converger eval 'Converger.Release.reset_admin_2fa("[email protected]")'.

Docker compose only​

These are read by docker-compose.yml (from .env, see .env.example), not by the application.

VariableRequiredMeaning
POSTGRES_PASSWORDyesPassword of the compose Postgres; also embedded in the app's DATABASE_URL. Use URL-safe characters.
SECRET_KEY_BASE, CLOAK_KEYyesPassed through to the migrate and app services.
GF_SECURITY_ADMIN_PASSWORDyesGrafana admin password.
PHX_HOST, FORCE_SSL, TRUSTED_PROXIES, METRICS_TOKEN, METRICS_ALLOWED_IPSnoPassed through; compose defaults PHX_HOST=localhost, FORCE_SSL=false.
RELEASE_COOKIE, CLUSTER_GOSSIP_SECRETwith docker-compose.cluster.ymlCookie and gossip secret of the two-node compose cluster.

Compose also sets PORT=4000, OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318, CREATE_DB=true for migrate and PHX_SERVER=true for app.

Test-only variables​

VariableUsed byMeaning
MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEYtest/converger/uploads/storage_integration_test.exs (--only minio)MinIO server for the S3 integration tests.
AZURITE_ENDPOINTsame file (--only azurite)Azurite blob endpoint, e.g. http://127.0.0.1:10000/devstoreaccount1.
CHECK_ORIGINtest/converger_web/check_origin_config_test.exsExercises the runtime parsing.
KAFKA_BROKERStest/converger/channels/kafka_integration_test.exs (--only kafka)Kafka bootstrap brokers, default localhost:9092. See running the broker tests.
RABBITMQ_URL, RABBITMQ_USERNAME, RABBITMQ_PASSWORDtest/converger/channels/rabbitmq_integration_test.exs (--only rabbitmq)RabbitMQ broker, default amqp://localhost:5672 with guest / guest.
NATS_URLtest/converger/channels/nats_integration_test.exs (--only nats)NATS server with JetStream, default nats://localhost:4222.
REDIS_URLtest/converger/channels/redis_streams_integration_test.exs (--only redis_streams)Redis server, default redis://localhost:6379.
MQTT_HOST, MQTT_PORTtest/converger/channels/mqtt_integration_test.exs (--only mqtt)MQTT broker without authentication, default localhost / 1883. See running the MQTT tests.

Application configuration (config/*.exs)​

These keys are set in config/config.exs (or read with a default when absent). Override them in config/runtime.exs or an environment file; none has an environment variable unless listed above.

Oban​

config :converger, Oban,
repo: Converger.Repo,
plugins: [
{Oban.Plugins.Pruner, max_age: 3600 * 24},
{Oban.Plugins.Lifeline, rescue_after: :timer.minutes(30)},
{Oban.Plugins.Cron,
crontab: [
{"0 * * * *", Converger.Workers.ConversationExpirationWorker},
{"*/5 * * * *", Converger.Workers.ChannelHealthWorker},
{"15 0 * * *", Converger.Workers.PartitionMaintenanceWorker},
{"30 1 * * *", Converger.Workers.PruneWorker},
{"0 2 1 * *", Converger.Workers.RetentionWorker},
{"*/10 * * * *", Converger.Workers.SessionPruneWorker}
]}
],
queues: [default: 10, deliveries_high: 10, deliveries: 20, deliveries_bulk: 5, maintenance: 1, webhooks: 10, backbone: 10]
SettingValueNotes
Queue default10 concurrent jobs per nodeConversation expiration (hourly) and channel health checks (every 5 minutes).
Queue maintenance1 concurrent job per nodeData lifecycle: partitions (daily 00:15 UTC), pruning (daily 01:30 UTC), retention (monthly, 1st at 02:00 UTC), expired admin/portal sessions and password reset tokens (every 10 minutes, SessionPruneWorker) and the PurgeWorker jobs enqueued by tenant, channel and conversation deletes. See Data retention.
Queues deliveries_high, deliveries, deliveries_bulk10 / 20 / 5 concurrent jobs per nodeConverger.Workers.ActivityDeliveryWorker (unique per activity and channel), one queue per tenant tier (tenants.tier: high, default, bulk); see flow control.
Queue webhooks10 concurrent jobs per nodeConverger.Workers.WebhookEventWorker: platform event webhooks.
Queue backbone10 concurrent jobs per nodeConverger.Workers.BackboneEventWorker (Native backbone subscriptions, and broker events whose handler failed) and Converger.Workers.BackbonePublishWorker (events parked while a broker was unreachable); see Event backbone.
Prunercompleted/discarded jobs older than 24 hKeeps oban_jobs small.
Lifelinerescues jobs stuck in executing after 30 minRecovers deliveries from a crashed node.
Testtesting: :inlineJobs run synchronously in tests.

The schema must be at Oban migration version 14 (migration 20261009131000); Oban 2.24 refuses to start otherwise. Jobs can be inspected at /admin/oban.

Delivery pipeline​

config :converger, pipeline: [backend: Converger.Pipeline.Oban]
BackendUse
Converger.Pipeline.ObanDefault. Deliveries are Oban jobs inserted in the activity transaction (durable). See ADR-0001.
Converger.Pipeline.BroadwayThroughput backend with broadway: [producer: :memory, :kafka, :rabbitmq or :custom, ...]. Not durable; the :memory producer refuses to start in prod unless allow_memory_producer_in_prod: true. See ADR-0002. This is the internal pipeline transport; to exchange conversations with other systems over Kafka or RabbitMQ use a broker channel instead.
Converger.Pipeline.InlineSynchronous; used by config/test.exs.

Retry defaults for external deliveries come from config :converger, :retry_policy (keys max_attempts 5, backoff :exponential, base_ms 10000, max_ms 1 h, timeout_ms 15000; the legacy base_backoff_seconds is still honoured), merged with adapter defaults and each channel's retry_policy. See ADR-0019.

Uploads​

config :converger, Converger.Uploads,
storage: Converger.Uploads.LocalStorage,
storage_opts: [dir: "priv/uploads"],
max_file_size: 10 * 1024 * 1024,
signed_url_ttl: 300,
cdn: nil

allowed_content_types: nil (the default) means the built-in allowlist. In releases this block is rebuilt from the file storage variables. Test uses a temp directory and a 1 MB limit.

Rate limiting​

config :converger, Converger.RateLimit,
backend: :local,
sync_interval_ms: 100,
override_cache_ttl_ms: 30_000,
limits: %{}

limits overrides built-in bucket defaults installation-wide, e.g. %{inbound: {1_000, 1_000}} ({limit, window_ms}). clean_period_ms (default 60000) controls how often expired ETS counters are removed. See Rate limiting.

Retention, partitions and archive​

config :converger, Converger.Partitions,
months_ahead: 3, # PARTITION_MONTHS_AHEAD
detach_concurrently: true, # false in test (the SQL sandbox is one transaction)
lock_timeout_ms: 5_000 # for creating, attaching and dropping partitions

config :converger, Converger.Retention,
min_retention_days: 30, # RETENTION_MIN_DAYS
health_check_days: 7, # HEALTH_CHECK_RETENTION_DAYS, 0 or nil disables
audit_log_days: 365, # AUDIT_LOG_RETENTION_DAYS, 0 or nil disables
webhook_event_days: 7, # WEBHOOK_EVENT_RETENTION_DAYS, 0 or nil disables
backbone_receipt_days: 7, # BACKBONE_RECEIPT_RETENTION_DAYS, 0 or nil disables
prune_batch_size: 10_000

config :converger, Converger.Archive,
storage: nil, # nil: the attachment storage (Converger.Uploads)
storage_opts: [],
prefix: "archive", # ARCHIVE_PREFIX
part_rows: 50_000 # ARCHIVE_PART_ROWS

Per-tenant retention is the tenants.retention_days column (default 365). See Data retention, partitions and archive.

Protocol v1 transports​

config :converger, ConvergerWeb.Protocol,
heartbeat_interval_ms: 30_000,
idle_timeout_ms: 60_000,
replay_max: 10_000

Settings of the native WebSocket (/socket/converger/v1), the gRPC stream (Protocol v1 over gRPC) and the Server-Sent Events stream, announced to clients in welcome.data.limits (Protocol v1, section 11). heartbeat_interval_ms: outbound silence before a heartbeat frame (also the SSE heartbeat period). idle_timeout_ms: inbound silence before the socket is closed with 4408. replay_max: frames replayed per handshake, sync or SSE connection before replayTruncated. The replay batch size is ws_replay_limit under Pagination. Keep the heartbeat below any proxy idle timeout in front of Converger. Frame size, message rate, slow-consumer and draining limits are the shared client WebSocket limits (config :converger, :websocket, WS_* variables), see WebSocket limits and draining.

Tokens passed as ?token= (sockets, SSE) are filtered from request logs by config :phoenix, :filter_parameters, ["password", "token", "secret"].

Repo and migrations​

config :converger, Converger.Repo,
migration_lock: :pg_advisory_lock,
migration_advisory_lock_retry_interval_ms: 1_000

Concurrent migration runners serialize on a Postgres advisory lock. Requires a session-mode connection (not PgBouncer transaction pooling). See Migrations and maintenance windows.

Other application keys​

KeyDefaultMeaning
:cors_origins["http://127.0.0.1:5500", "http://localhost:5500"]See CORS_ORIGINS.
:admin_ip_whitelist["127.0.0.1", "::1"]See ADMIN_IP_WHITELIST.
:admin_api_tokennilSee ADMIN_API_TOKEN.
:trusted_proxies[]See TRUSTED_PROXIES.
:sessionsmax_age_seconds: 43_200, idle_timeout_seconds: 1_800See SESSION_MAX_AGE_SECONDS, SESSION_IDLE_TIMEOUT_SECONDS.
:password_min_length12See PASSWORD_MIN_LENGTH.
:require_2fa_roles["super_admin"]; [] in testSee ADMIN_REQUIRE_2FA_ROLES.
:mailer_from{"Converger", "[email protected]"}See MAILER_FROM.
:inbound_signature_tolerance_seconds300Allowed clock skew for x-converger-signature timestamps.
:password_hasherBcrypt; Pbkdf2 with CONVERGER_PASSWORD_HASHER=pbkdf2Comeonin module that hashes new admin and tenant user passwords. See Password hashing.
:paginationsee PaginationPage size defaults and caps.
:dead_lettersbulk_retry_limit: 10_000, export_limit: 10_000Max deliveries replayed by one bulk retry call, and max rows in one Deliveries CSV export. See Replaying dead letters.
:circuit_breakerfailure_threshold: 5, cooldown_ms: 30_000, park_seconds: 600, max_in_flight: :auto, busy_snooze_ms: 1_000, replay_dead_letters_on_close: false, replay_window_ms: 3_600_000Per-channel delivery circuit breaker and opt-in dead-letter replay on close. See Circuit breaker.
:adapters[]Channel adapter modules registered after the built-in ones (echo, webhook, websocket, whatsapp_meta, whatsapp_infobip, slack, telegram, email, sms_twilio, sms_vonage, messenger, instagram, viber, discord, teams, http_pull, kafka, rabbitmq, mqtt). One with a built-in type replaces it. Checked at boot. See Writing an adapter.
:channel_healthprobe_idle_channels: true; false in testCall the adapter's health_probe/1 for channels without deliveries in the health window. See Health probes.
Converger.Channels.MediaDownloadtimeout_ms: 10_000Receive timeout of each inbound media download (WhatsApp images, voice notes, documents). See inbound media download.
Converger.Channels.Templatesttl_ms: 300_000How long a channel's provider templates are cached per node. See channel message templates.
:metricstenant_label: false, poll_interval_ms: 15_000 (false in test); dev adds allowed_ips with loopback and private rangestoken: and allowed_ips: for GET /metrics (see METRICS_TOKEN); tenant_label: and poll_interval_ms: (see METRICS_TENANT_LABEL, METRICS_POLL_INTERVAL_MS; poll_interval_ms: false turns the database-backed gauges off).
:prometheus_portunsetPort of the optional unauthenticated metrics listener (PROMETHEUS_PORT).
Converger.Healthdb_timeout_ms: 1000Readiness database timeout.
Converger.Repo.Poolsbackground: false; dev [pool_size: 10]; prod from BACKGROUND_*The background pool (keyword list of repo options) or false for one shared pool. See Database pools and pressure.
Converger.Repo.Pressureretry_after_ms: 1000, queue_window_ms: 5000, degraded_queue_ms: 200See DB_RETRY_AFTER_MS, DB_QUEUE_WINDOW_MS, READY_DB_QUEUE_MS.
:force_sslunset outside prodKeyword list of Plug.SSL options, built from the TLS variables in prod; false disables.
:platform_webhookssee aboveenabled, connect_timeout, receive_timeout, max_response_bytes of platform event webhooks.
:webhook[]allowed_targets, allow_private_targets, resolver (SSRF guard), and installation defaults for connect_timeout (5000 ms), receive_timeout (10000 ms), max_response_bytes (1 MiB).
:channel_signals_asynctrue; false in testForward typing indicators and read receipts to external channels (Converger.Channels.Signals) in a task under Converger.TaskSupervisor. false runs them inline in the WebSocket channel process.
:webhook_req_options, :platform_webhook_req_options, :whatsapp_req_options, :slack_req_options, :telegram_req_options, :email_req_options, :sms_req_options, :messenger_req_options, :instagram_req_options, :viber_req_options, :discord_req_options, :teams_req_options[]Extra Req options merged into adapter (and platform webhook) requests (tests use them for Req.Test plugs). :email_req_options applies to the SNS certificate and subscription requests of email channels.
:email_swoosh_adapternilA Swoosh adapter module used instead of the provider adapter of every email channel. The test config sets Swoosh.Adapters.Test. See Email.
Converger.Channels.Adapters.WhatsappMeta, graph_api_version:"v26.0"Default Graph API version when the channel config has none.
Converger.Channels.Adapters.Telegram, api_base_url:"https://api.telegram.org"Bot API base URL for every telegram channel, for a self-hosted Bot API server. Not a channel setting: the URL receives the bot token.
Converger.Channels.Adapters.Messenger / .Instagram, graph_api_version:"v26.0"Default Graph API version of messenger / instagram channels without one in their config.
Converger.Channels.Adapters.Viber, api_base_url:"https://chatapi.viber.com/pa"Viber Bot API base URL for every viber channel. Not a channel setting: the URL receives the token.
Converger.Channels.Adapters.Discord, api_base_url:"https://discord.com/api/v10"Discord API base URL for every discord channel. Not a channel setting: the URL receives the bot token.
Converger.Channels.Adapters.Discord, gateway_url:, fatal_retry_ms:unset (the url of GET /gateway/bot), 300000Gateway URL of every discord channel in Gateway mode (tests point it at a fake Gateway; the URL receives the bot token), and the wait before reconnecting after a close code no retry fixes (4004, 4010 to 4014).
Converger.Channels.Adapters.Teams, openid_metadata_url:, issuers:, login_base_url:, service_url_hosts:, key_refresh_cooldown_ms:Bot Framework public cloudBot Framework token validation, the OAuth token endpoint and the hosts replies may be posted to; see Microsoft Teams.
:activity_limitsmax_text_bytes: 65_536, max_attachments: 10, max_attachment_bytes: 4_096, max_metadata_bytes: 16_384Activity size limits (attachment and metadata sizes measured as JSON).
:conversation_inactivity_hours24Open conversations idle longer than this are closed by the hourly expiration job.
:inbound_conversation_idle_timeout_secondsunset (no idle timeout)Global default for starting a new conversation for an inbound participant after inactivity; channels override it with conversation_idle_timeout_seconds in their config.
:api_key_rotation_grace_period86400 (seconds)How long the previous tenant API key keeps working after a rotation.
:token_signing_keys, :token_rotation_grace_seconds, :token_accept_unkeyed[], 30, trueSee TOKEN_SIGNING_KEYS, TOKEN_ROTATION_GRACE_SECONDS, TOKEN_ACCEPT_UNKEYED.
Converger.Auth.RevocationCachesync_interval_ms: 60_000 (nil in test)How often each node re-reads recently revoked tokens from revoked_tokens, in case a PubSub broadcast was missed. See Client tokens.
:extra_middleware%{}Map of middleware type string to module, merged into the built-in middleware registry.
Converger.Clusterstrategy: :nonestrategy: and libcluster options: built from the CLUSTER_* variables; see Clustering.
:dev_routestrue in devCompile-time. Mounts the Swoosh mailbox preview at /dev/mailbox.
:envconfig_env()Used for environment-specific guards (for example the Broadway memory producer).

Logging​

config/config.exs uses the console formatter with request_id metadata. config/prod.exs sets the level to :info and installs LoggerJSON on the default handler with key redaction; config/test.exs logs warnings and above. See Observability.

Mailer​

Converger.Mailer sends the account emails (password reset). It uses Swoosh.Adapters.Local (dev mailbox at /dev/mailbox) and Swoosh.Adapters.Test in test. Production sets the Req API client; with SMTP_HOST set it uses Swoosh.Adapters.SMTP (Accounts, sessions and email), otherwise no email is delivered.