Skip to main content

Channels

A channel is a tenant's endpoint to the outside world: a webhook URL, a WhatsApp number, a set of WebSocket clients. Its type selects the adapter that talks to the provider. Its mode says whether messages may come in, go out, or both. A conversation always belongs to exactly one channel. Other channels can receive its activities through routing rules.

Source: lib/converger/channels/channel.ex, lib/converger/channels.ex, lib/converger/channels/adapter.ex.

Schema​

Table channels:

FieldTypeDefaultNotes
iduuidPrimary key.
tenant_iduuidOwner. (tenant_id, name) is unique.
nametextRequired.
typestring"webhook"Adapter: echo, webhook, websocket, whatsapp_meta, whatsapp_infobip, slack, telegram, email, sms_twilio, sms_vonage, messenger, instagram, viber, discord, teams, http_pull, kafka, rabbitmq, nats, redis_streams, mqtt.
modetext"duplex"inbound, outbound or duplex. Must be supported by the type.
secretencrypted binarygeneratedEncrypted at rest. Generated (32 random bytes, Base64) when not given. Rotate secret in the admin panel or portal (Channels.rotate_secret/2) replaces it; the old secret stops working at once.
secret_hashbinarySHA-256 of secret, unique, used for lookups.
configencrypted map{}Adapter configuration, including provider credentials. Encrypted at rest.
transformationsjsonb array[]Middleware chain applied before delivery to this channel.
require_signaturebooleantrueReject unsigned inbound webhooks.
retry_policymap{}Per-channel delivery retry overrides.
rate_limitstringnilOutbound rate limit ("80/s", "1000/m", "5000/h"); nil uses the adapter default (WhatsApp Meta: 80/s). See flow control.
circuit_statestring"closed"Delivery circuit breaker: closed, open, half_open or paused. Managed by Converger.Channels.Circuit, never set through the changeset.
circuit_changed_at, consecutive_failuresdatetime, integernil, 0Breaker bookkeeping.
statustext"active"The admin panel and portal toggle between active and inactive.
inserted_at, updated_atutc_datetime_usec

All fields go through Channel.changeset/2, which validates the type, the mode (and that the type supports it), the type's config, the signature config, the middleware chain and the retry policy.

Types and adapters​

Each type maps to a module implementing the Converger.Channels.Adapter behaviour: deliver_activity/2, parse_inbound/2, validate_config/1, supported_modes/0, and optionally parse_status_update/2, verify_inbound_signature/3, retry_policy/0 and capabilities/0 (default [:inbound, :outbound]; the pipeline delivers only to types with :outbound).

TypeModesRequired configDeliveryPage
webhookinbound, outbound, duplexurl. Optional: method (POST, PUT, PATCH), headers, connect_timeout, receive_timeout, max_response_bytesHTTP request with the canonical activity, signed with x-converger-signatureWebhooks
websocketinbound, outbound, duplexnone. Optional: require_ack (true/false, default false)Broadcast to the channel's connected sockets; the delivery stays pending while no client is connected (or until a client acks, with require_ack). Clients send messages over the socket (ADR-0033).WebSocket channel
whatsapp_metainbound, outbound, duplexphone_number_id, access_token, verify_token, plus app_secret when require_signature is trueWhatsApp Cloud API (Graph)WhatsApp
whatsapp_infobipinbound, outbound, duplexbase_url, api_key, senderInfobip WhatsApp APIWhatsApp
slackinbound, outbound, duplexbot_token, signing_secret. Optional: channel, app_id, conversation_scope (thread, channel, user), download_filesSlack Web API (chat.postMessage, chat.update, chat.delete, file uploads); Events API inboundSlack
telegraminbound, outbound, duplexbot_token, plus secret_token when require_signature is true. Optional: parse_mode, allowed_updatesTelegram Bot API (sendMessage, sendPhoto, sendDocument)Telegram
emailinbound, outbound, duplexprovider (smtp, ses, sendgrid, mailgun, postmark), from, the provider credentials, plus ses_topic_arn or mailgun_signing_secret for those inbound providersEmail through Swoosh, threaded with Message-ID / In-Reply-ToEmail
sms_twilioinbound, outbound, duplexaccount_sid, auth_token, and from or messaging_service_sid. Optional: webhook_base_url, default_country_code, max_segmentsTwilio Programmable Messaging (SMS, MMS), status callbacksSMS
sms_vonageinbound, outbound, duplexfrom, and api_key + api_secret or application_id + private_key, plus signature_secret when require_signature is true. Optional: default_country_code, max_segmentsVonage Messages API (SMS), status webhooksSMS
kafkainbound, outbound, duplexbrokers, plus topic_in (inbound, duplex) and topic_out (outbound, duplex). Optional: group_id, start_offset, dlq_topic, concurrency, SASL (sasl_mechanism, sasl_username, sasl_password), TLS (tls, tls_verify, tls_ca_cert)Produces the activity envelope to topic_out keyed by conversation; consumes topic_in (broker channel)Kafka
rabbitmqinbound, outbound, duplexurl, plus queue (inbound, duplex) and routing_key (outbound, duplex). Optional: username, password, exchange, dlq_queue, declare, prefetch_count, concurrency, tls_verify, tls_ca_certPublishes the activity envelope with publisher confirms; consumes queue (broker channel)RabbitMQ
natsinbound, outbound, duplexservers, plus stream and subject_in (inbound, duplex) and subject_out (outbound, duplex). Optional: durable_name, dlq_subject, declare, deliver_policy, batch_size, ack_wait_seconds, concurrency, auth (username / password, token, nkey_seed or creds), tls, tls_verify, tls_ca_certJetStream publish of the activity envelope; pulls from a durable consumer (broker channel)NATS JetStream
redis_streamsinbound, outbound, duplexurl, plus stream_in (inbound, duplex) and stream_out (outbound, duplex). Optional: username, password, group, dlq_stream, maxlen, start_id, batch_size, concurrency, tls_verify, tls_ca_certXADD of the activity envelope; reads stream_in with a consumer group (broker channel)Redis Streams
mqttinbound, outbound, duplexhost, plus topic_in (inbound, duplex) and topic_out (outbound, duplex). Optional: port, tls, tls_verify, tls_ca_cert, username, password, client_id, topic_dlq, qos, keepalive, derive_idempotency_keyPublishes the activity envelope with QoS 1 to the conversation's topic; subscribes to topic_in in a persistent session (broker channel)MQTT
echooutboundnoneWrites a reply activity from "bot" into the same conversation (testing)Echo

See the channels overview for provider setup, and writing an adapter to add a type. Each type is one adapter module: the type list, the config validation above and the admin form all come from the adapters' capabilities/0 and config_schema/0 (ADR-0038).

One config key is read for every inbound-capable type: conversation_idle_timeout_seconds (positive integer). It starts a new conversation for a participant whose active conversation has been idle longer than this (participants).

Modes​

The mode is the channel's direction. It was introduced by migration 20260227160000_add_mode_to_channels, which defaulted existing channels to duplex and set echo and websocket channels to outbound. Migration 20261010200000_make_websocket_channels_duplex later moved existing websocket channels from outbound to duplex, when the type gained inbound support (#22).

ModeAccepts inbound webhooksReceives deliveriesCan be a routing rule sourceCan be a routing rule target
inboundyesnoyesno
outboundnoyesnoyes
duplexyesyesyesyes

Concretely:

  • POST /api/v1/channels/:id/inbound with messages on an outbound channel returns 400 inbound_not_supported (errors). If the same request also carried status updates, they are applied, and the messages are dropped with a warning. A new_activity sent over the client API socket of an outbound channel is answered with the error inbound_not_supported.
  • The pipeline delivers only to channels whose mode is outbound or duplex, both for the conversation's own channel and for routing targets.
  • RoutingRules rejects an outbound source ("source channel is outbound-only and cannot receive inbound messages") and inbound targets.
  • Channel.changeset/2 rejects a mode the adapter does not support, for example echo channels only support modes: outbound.

Secret​

Every channel has a secret, shown once when the channel is created in the admin panel and stored encrypted with Converger.Vault (AES-GCM, key CLOAK_KEY). Lookups go through secret_hash, and the decrypted value is then compared in constant time. The secret is used to:

  • authenticate POST /api/v1/converger/tokens/generate (authorization: Bearer <channel secret>), which issues client tokens for the channel (client API);
  • verify the generic inbound signature x-converger-signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")> (ADR-0009);
  • sign outbound webhook requests with the same header format (ADR-0014).

Config and encryption at rest​

config is a map with string keys, validated by the adapter's validate_config/1. Both secret and config are Cloak.Ecto encrypted fields. Migration 20261008100000_encrypt_channel_secrets encrypted existing rows in place and dropped the plaintext columns (issue #12). In the admin UI and the audit logs, sensitive keys (access_token, api_key, secret, token, password, verify_token, app_secret, authorization, and keys ending in _secret, _token, _hash or _password) are masked or redacted.

After rotating CLOAK_KEY, move the old key to CLOAK_RETIRED_KEYS and run bin/converger eval "Converger.Release.reencrypt_secrets()". See ADR-0012 and security.

Transformations​

transformations is an ordered list of middleware steps, each a map with a "type" and its options:

[
{ "type": "add_prefix", "prefix": "[Support] " },
{ "type": "truncate_text", "max_length": 160, "ellipsis": "..." }
]

The chain is validated on save (unknown types and bad options are rejected). It runs for every delivery to this channel, just before the adapter. The admin channel form has a "Middleware Pipeline" editor. See middleware.

Signature enforcement​

require_signature controls unsigned inbound requests on /inbound and /status:

Requestrequire_signature: truerequire_signature: false
Valid current signature (t=...,v1=..., or the provider's native scheme)acceptedaccepted
Valid legacy signature (sha256=<hex>, no timestamp)401accepted, deprecation warning logged
No signature401accepted, deprecation warning logged
Invalid signature401401

New channels default to true. Migration 20261008120000_add_require_signature_to_channels backfilled existing channels with false, so old integrations kept working. whatsapp_meta verifies Meta's X-Hub-Signature-256 with config["app_secret"], and cannot require signatures without it. slack verifies X-Slack-Signature with config["signing_secret"]. sms_twilio verifies X-Twilio-Signature with config["auth_token"], and sms_vonage the signed-webhook JWT with config["signature_secret"]. Timestamped signatures must be within config :converger, :inbound_signature_tolerance_seconds (default 300) of the server clock. Toggle the flag in the channel table ("Signature: required/optional"). Details: webhooks.

Retry policy​

retry_policy overrides delivery retries for this channel. The effective policy is resolved by Converger.Pipeline.RetryPolicy.for_channel/1, which merges, in order: the global defaults (config :converger, :retry_policy), the adapter's retry_policy/0 defaults, and the channel's own map.

KeyDefaultMeaning
max_attempts5Attempts before the delivery is dead-lettered (failed).
backoffexponentialexponential (base_ms * 3^attempt), linear (base_ms * attempt) or fixed (base_ms).
base_ms10000Backoff base in ms.
max_ms3600000 (1 h)Backoff cap in ms.
timeout_ms15000 (webhook: 10000)Adapter request timeout.
{ "max_attempts": 8, "backoff": "linear", "base_ms": 5000, "max_ms": 600000 }

Values must be positive integers (or a valid backoff name), and unknown keys are rejected. With the defaults, retries come about 30 s, 90 s, 270 s and 810 s after attempts 1 to 4. A provider Retry-After overrides the backoff for the next attempt. See deliveries and ADR-0019.

Status​

A channel is active or inactive. When a channel stops being active:

  • its connected client sockets are disconnected (ConvergerWeb.Sockets.disconnect_channel/1), and cannot reconnect or rejoin while it stays inactive;
  • inbound webhooks get 400 channel_inactive;
  • client-API requests with its tokens get 403 channel_inactive ("Channel not found or inactive", error envelope), and tokens/generate with its secret fails;
  • new conversations cannot be created on it through POST /api/v1/conversations;
  • it is skipped as a routing rule target.

Deleting a channel also disconnects its sockets, and cascades to its conversations, participants, deliveries and health checks.

Health checks​

Converger.Workers.ChannelHealthWorker runs every 5 minutes (Oban cron). For each active channel of type webhook, whatsapp_meta, whatsapp_infobip, slack, telegram, email, sms_twilio, sms_vonage, kafka, rabbitmq, nats, redis_streams or mqtt (every type with the :external_delivery capability), it computes the delivery failure rate over the last 60 minutes and stores a row in channel_health_checks:

StatusFailure rate (failed / total deliveries in window)
healthybelow 10%
degraded10% to below 50%
unhealthy50% or more
unknownno deliveries in the window

When the status differs from the previous check, the worker logs the change, broadcasts health_changed on the channel_health PubSub topic (the admin channel list shows a health dot), and POSTs to the tenant's alert webhook. Checks older than 7 days are pruned on every run. A channel turning unhealthy opens its delivery circuit breaker (see Delivery: circuit breaker).