Skip to main content

Architecture decision records

An Architecture Decision Record (ADR) captures one architecturally significant decision: the problem and the forces at play, the options that were on the table, what was chosen and why, and what it costs. Converger keeps its ADRs in docs/adr/, next to the code, in the MADR format.

ADRs are immutable once accepted. When a decision changes, a new ADR supersedes the old one and both are linked. Read How we write ADRs before adding one, and start from the template.

Status values​

StatusMeaning
ProposedUnder discussion in an issue or pull request.
AcceptedDecided and, unless noted, implemented on main.
DeprecatedNo longer relevant (the feature was removed).
Superseded by ADR-NNNNReplaced by a later decision.

Index​

Delivery and data integrity​

ADRDecisionStatusIssue / PR
0001Transactional outbox: Oban delivery jobs are inserted in the activity transactionAccepted#1 / #69
0002Broadway for throughput, Oban for retries and dead lettersAccepted#2 / #70
0003The pipeline is the only delivery pathAccepted#3 / #71
0004One canonical activity serializer for REST, sockets and broadcastsAccepted#4 / #72
0005Separate client and system changesets for activitiesAccepted#5 / #73
0006Per-conversation seq under a row lock and opaque seq watermarksAccepted (opaque watermarks superseded by ADR-0068)#6 / #78
0015Per-message idempotent processing of inbound batchesAccepted#15 / #82
0016Participant-based conversation resolution for inbound messagesAccepted#16 / #88
0017Conversation lifecycle enforced under the seq lockAccepted#17 / #84
0018Keyset pagination for every list queryAccepted#18 / #89
0019Per-channel retry policy, DeliveryError and Oban LifelineAccepted#19 / #85
0028Dead-letter replay resets the delivery in place and always goes through ObanAccepted#32 / #121
0031Per-channel circuit breaker on the channel row, priority-based parking, Hammer rate limits and tier queuesAccepted#31 / #120
0076When a channel's breaker opens or it is paused, its whole waiting backlog is parked in one statement and new deliveries are inserted parked; at most max_in_flight deliveries of one channel run at once per node (default half of the queue's workers), and a channel at its cap has its waiting jobs demoted to priority 2Accepted#191 / (this PR)
0034Monthly partitioning of activities and deliveries, per-tenant retention and verified archivesAccepted#30 / #124
0036Rich activity model: closed type vocabulary, validated attachments, append-only edits and deletes, adapter capabilities with downgradeAccepted#28 / #125
0062Typed message payloads: rich message and mekik-native frames stored losslessly as message activities with message_type and payload, validated against the Protocol v1 schemas, text fallback and message_types capability for channelsAccepted#68 / #166
0064Per-channel rendering of typed messages: native buttons, lists, keyboards and Block Kit on WhatsApp, Telegram, Slack and email, numbered-list text elsewhere, and answers normalised to one user turn through opaque answer keys and numeric repliesAccepted (genui joining rule superseded by ADR-0072)#68 / (this PR)
0070Pluggable event backbone: platform events through Converger.Backbone with Native (PubSub + Oban, default), Kafka, NATS JetStream and RabbitMQ selected by one config value, idempotency receipts in the handler's transaction, broker outages parked in Postgres, dual publish for migrations; deliveries and the WebSocket fan-out stayAccepted#66 / (this PR)
0071activity.created is published in the transaction that stores the activity (transactional outbox on every backbone: Native subscription jobs, a publish job for brokers), instead of after the commit, which lost events when a node died in betweenAccepted#66
0072A GenUI stream joined into one message for a channel without GenUI concatenates the text chunks that share a chunk id (a text run, as mekik streams and chativa renders them) instead of keeping the last one; runs are trimmed and joined by blank linesAccepted#67 / (this PR)

Channels, middleware and real time​

ADRDecisionStatusIssue / PR
0007Attachment storage with hand-written Req signing, CDN layer and authenticated downloadsAccepted#7 / #80
0008Middleware receives the channel; middleware crashes are containedAccepted#8 / #74
0020Per-subject socket ids and PresenceAccepted#20 / #81
0024Converger Protocol v1 is a superset profile of mekik/1Accepted#63
0026One client socket stack; the legacy socket and token family are deprecatedAccepted#23 / #115
0027WebSocket limits in the socket transport, mailbox backpressure and readiness-gated drainingAccepted#27 / #117
0029Protocol v1 sends on the Phoenix binding are acked in the channel process, with an in-flight window counted from its mailboxAccepted#24 / #127
0063Channel-scoped native v1 connections serve every routed conversation in {conversationId, frame} envelopes; a bot channel is a routing target with a connected agent; bot-only frames, transient bot frames and meta relayed over PubSub; interrupt answers become resume; hello.group shares conversations by rendezvous hashing over :pgAccepted#64
0069Agent group members own the user turns committed after their published eligibility time (join plus group_settle_ms), decided from the turn's inserted_at by every member alike; on a member's departure the new owner receives the unanswered turns that were passed to itAccepted#67 / (this PR)
0066The mekik channel adapter (Mode A): one supervised, :global Mint WebSocket per conversation to a stock mekik/1 server, the mekik conversation and watermark recorded in mekik_sessions to resume, persistent frames stored exactly once by seq idempotency keys, a per-node relay for bot-only frames and turn meta, a :bot_relay adapter capabilityAccepted#64
0030Native v1 WebSocket on WebSock, MessagePack by subprotocol, SSE and long-poll fallbacksAccepted#26 / #122
0032Transient conversation signals: receipts, typing and presence over dedicated topics, stored read watermarks, optional adapter callbacksAccepted#25
0033The websocket channel is a delivering adapter with pending receiptsAccepted#22 / #119
0035Native transports share the signals core, the client WebSocket limits and drainingAccepted#26, #25, #27 / #123
0077Protocol v1 handshakes and sync replays wait for one of a bounded number of per-node slots (half the pool by default) and read through one pool checkout, so a drained node's clients queue on the survivors instead of failing in the pool; the TypeScript SDK caps its backoff at 10 s and the load harness honours retryAfterMsAccepted#198 / (this PR)
0056Protocol v1 over a gRPC bidirectional stream on its own port; one transport-agnostic session for the WebSocket and gRPC, lossless JSON-protobuf frame mapping with a raw Struct fallback, a Close frame plus a matching gRPC status for endingsAccepted#44 / #156
0038Adapter behaviour v2: declared capabilities and config schemas, a registry from built-ins plus config :converger, :adaptersAccepted#36 / #130
0039WhatsApp rich outbound messages through a shared planner, synchronous inbound media download into Converger storage, cached template listingAccepted#37 / #134
0040Slack adapter: <channel>:<ts> message refs, conversation_scope thread / channel / user, an in-band inbound_challenge/2 callbackAccepted#39 / #131
0041Telegram adapter: chat-scoped message ids, a configured secret token is always enforced, inbound files stored as uploads, register_webhook/2 behind an admin buttonAccepted#38 / #132
0042Email adapter: deterministic Message-ID per activity, thread-first conversation resolution (thread_refs), provider webhooks with per-provider authentication (SNS signature and topic, Mailgun HMAC, basic auth)Accepted#40 / #133
0045SMS adapters (Twilio, Vonage): a participant opted_out_at flag the pipeline checks before every delivery, Twilio signatures checked against candidate public URLs, an optional inbound_response/1 callback (TwiML)Accepted#41 / #136
0046Conditional routing rules (JSON conditions validated on save, priority with first-match stop, per-rule middleware), reply-back routing by the activity's source channel, conversations spanning channels, and a dry-run on the real plannerAccepted#43 / #138
0047Broker channels: kafka and rabbitmq channel types, one Broadway consumer per channel in the cluster (:global plus a reconciling manager), broker-message-id idempotency, per-channel dead-letter topic / queue; the Broadway pipeline transport stays internalAccepted#42 / #141
0048Discord, Teams, Messenger, Instagram, Viber and HTTP pull: shared Meta Graph code, cached Bot Framework JWT validation with a service URL allowlist, users as participants with reply targets in metadata, inbound_response/2, a pull outbox acknowledged by served-only watermarksAccepted#44 / #139
0054NATS JetStream and Redis Streams broker channels on the ADR-0047 machinery: gnat and redix with small Broadway producers of our own (a durable pull consumer; a consumer group with XAUTOCLAIM recovery on start), stream sequence / entry id as the broker message id, per-channel dead-letter subject / streamAccepted#42 / #155
0053Platform event webhooks: tenant subscriptions to a fixed event catalog emitted from the existing broadcast hooks, one webhook_events row and Oban job per subscription, outbound signing and SSRF guard, shared retry policy, per-subscription dead letters and redelivery, 7-day log; alert_webhook_url becomes a managed subscriptionAccepted#49 / #143
0055MQTT channel on an in-house MQTT 3.1.1 client that acknowledges QoS 1 messages after the commit, persistent session per channel, idempotency key from the envelope or a SHA-256 of topic and payload; Discord Gateway inbound as an optional mode run by the broker consumer infrastructure, which gets a per-adapter child spec and a per-channel consume?/1Accepted#44 / #157

Clients and SDKs​

ADRDecisionStatusIssue / PR
0065chativa's unmodified connector-mekik is the reference WebSocket client, kept working by a CI suite that runs its vendored code in Node against a real server; mekik/1 clients (no subprotocol, no hello.protocol) get a profile without Converger-only frames and heartbeats as pings; REST history pages backwards with a seq cursor (format=frames), channel-scoped tokens list conversationsAccepted#65 / (this PR)
0067Agent SDKs share one onTurn(turn, reply) API over the channel-scoped socket: reply.stream as genui text chunks, reply.ask as interrupt/resume, run frames around handlers, agent groups, watermark resume and acked sendsAccepted#67 / (this PR)
0068The watermark is the integer seq on the Converger REST API, the tenant API and every v1 surface; inputs accept the integer, its decimal string and the pre-v1 opaque forms for one release, reported as the deprecated opaque_watermark surface; only the legacy Phoenix activitySet keeps the stringProposed#63 / (this PR)
0052Server-side SDKs (Python, Go, Elixir) in packages/ of this repository, a shared conformance suite (REST, errors, protocol resume state machine, webhook signature vectors generated from the server signer), generator-shaped hand-written models until #45, per-language tag releasesAccepted#47 / #144
0051TypeScript SDK @converger/client: one session core over native / Phoenix / SSE transports, a persistent clientId outbox resent once after the replay, hand-written types in a generator-compatible shape until #45Accepted#46 / #142

Security​

ADRDecisionStatusIssue / PR
0009Inbound signature scheme t=,v1= and per-channel require_signatureAccepted#9 / #77
0011A custom trusted-proxies plug instead of the remote_ip libraryAccepted#11 / #75
0012Secrets at rest (Cloak AES-GCM), hashed tenant API keys, recursive audit redactionAccepted#12 / #79
0013Cluster-wide rate limiting with Hammer 7 ETS and PubSub syncAccepted#13 / #83
0014Webhook SSRF guard with DNS pinning and outbound signingAccepted#14 / #87
0058Admin and portal logins as hashed server-side session rows (absolute and idle expiry, revocable, LiveView disconnect), TOTP 2FA with nimble_totp (encrypted secret, single-use codes, required for super_admin by default), single-use hashed email reset tokens, one password policy (12 characters, 72 bytes, no email)Accepted#52 / #160
0059Client tokens: dedicated signing keys with kid, Postgres revocation list cached in ETS per node and fanned out over PubSub (rejected cluster-wide within 1 s), refresh rotation with a grace period and family-wide reuse detection, perms claim (read, write, typing)Accepted#52 / #158
0061Scoped tenant API keys (read, write, manage) in api_keys next to the full-access primary key, scope checks in TenantAuth that fail closed, /api/v1/manage for new configuration resources and manage on the existing ones, an ADMIN_API_TOKEN admin API, ETags with If-MatchAccepted#51 / #164
0074Publishable keys for sites without a backend: a per-channel cvg_pk_ key usable only from allowed origins mints an anonymous user and a token bound to a new conversation (src: "publishable"), continued with a signed resume credential; own per-IP and per-channel limits; CORS on the client API follows the keys' origins; test tokens from the channel pageAccepted#195 / #197

Platform, operations and process​

ADRDecisionStatusIssue / PR
0010CORS origins and OpenTelemetry export configured at runtimeAccepted#10 / #76
0021CI quality gates and LF line endingsAccepted#54 / #92
0022Deployment hardening: locked one-shot migrations, runtime ForceSSL/HSTS, fail-fast on leaked secretsAccepted#55 / #90
0023Platform and dependency baseline (OTP 28 / Elixir 1.19, Req 0.7, LoggerJSON, Oban 2.24)Accepted#56 / #91, #93, #104, #105
0025Docusaurus documentation site and documentation with every changeAccepted#48
0037libcluster clustering (runtime strategy), /health/live and /health/ready, metrics on the main port behind a token, two-node :peer suiteAccepted#29 / #129
0043Delivery SLO telemetry on the adapter call, polled database gauges, and alert rules as tested Prometheus rulesAccepted#33 / #135
0044Oban on a second (background) pool of the same repo; retryable 503 / unavailable / replay_unavailable instead of crashes under database pressure; readiness degraded on pool waitAccepted#35 / #137
0049k6 load harness on the release image (1 or 3 nodes), seq-based lost-message checks, results in docs/benchmarks/, nightly 15 % p95 regression gateAccepted#34 / #140
0050OpenAPI 3.1 generated from open_api_spex annotations and committed (CI fails on drift), Swagger UI at /api/docs, one error envelope {code, message, details, request_id} with Protocol v1 codes, URL versioning with Deprecation / Sunset and a 12-month windowAccepted#45 / #145
0057Password hashing behind Converger.Accounts.PasswordHasher: bcrypt by default, PBKDF2 (pure Elixir) when CONVERGER_PASSWORD_HASHER=pbkdf2 also drops the bcrypt NIF from the build, verification by the stored hash's prefix; .devcontainer/ with the pinned toolchain and PostgresAccepted#53 / #159
0060Admin panel and tenant portal stay LiveView, styled with Tailwind v4 + daisyUI 5 + heroicons (the Phoenix 1.8 stack) and built by the standalone tailwind/esbuild binaries: no Node.js, no CDN, one root layout with <Layouts.app>, light and dark themesAccepted#50 / #162
0073MIT license (inbound = outbound, no CLA) on the repository and every SDK package; SECURITY.md, Contributor Covenant code of conduct and issue formsAccepted#48 / #126