Skip to main content

ADR-0026: One client socket stack; the legacy socket and token family are deprecated

StatusAccepted
Date2026-10-09
Issue#23
Pull request#115
RelatedADR-0003, ADR-0006, ADR-0020, ADR-0024

Context and problem statement​

Converger had two client WebSocket stacks and two token families:

LegacyConverger API
Socket/socket, ConvergerWeb.UserSocket/socket/converger, ConvergerWeb.ConvergerSocket
Topic / channelconversation:<id>, ConversationChannelconverger:conversation:<id>, ConvergerChannel
TokenConverger.Auth.Token (x-channel-token, POST /api/v1/tokens)Converger.Auth.ConvergerToken (Bearer, /api/v1/converger/tokens/*)
Framesnew_activityactivitySet with a watermark
Resumelast_activity_idwatermark
Sendingnew_activity push with a replyREST only

They duplicated authorization, drifted in payload shape, and every document and SDK had to explain both. Protocol v1 (#21, ADR-0024) is built on the Converger API stack (#22), so the legacy stack has no future. The only capability the Converger API socket lacked was sending with a reply.

While moving the tests, we found that the legacy verifier (Converger.Auth.Token.verify_token/1) only checked the signature. All tokens share one signer, so an end user's conversation token or Converger API token passed x-channel-token authentication: it acted as the whole tenant on the tenant API (routing rules, any conversation's activities) and could call POST /api/v1/tokens to mint tokens for other conversations of the channel under any user id.

Decision drivers​

  • One implementation of the client protocol, so #22 to #28 are built once.
  • Existing integrations keep working until a removal date that is announced in advance (protocol v1, section 13.3).
  • Operators can see who still uses a deprecated surface before it is removed.
  • An end user's token must never act as the tenant.

Considered options​

  1. Deprecate the legacy stack, add sending to the Converger API socket, log every use - the legacy code stays as it is until removal.
  2. Make the legacy socket a thin alias of the Converger API socket - translate new_activity / last_activity_id into the new stack.
  3. Remove the legacy stack now - one breaking release.

Pros and cons of the options​

Option 1​

  • Good, because no existing client breaks before the announced removal.
  • Good, because the new code (sending) lives only in the stack that stays.
  • Bad, because the legacy code stays in the tree for at least two minor releases.

Option 2​

  • Good, because only one channel module would run.
  • Bad, because the translation layer is new code with its own bugs, written for a surface that is removed anyway, and its frames (new_activity with the canonical map) still differ from activitySet.

Option 3​

  • Good, because it removes the most code.
  • Bad, because it breaks every legacy client without a migration window, against the deprecation policy of protocol v1.

Decision​

Chosen option: "Deprecate, add sending, log every use".

  • ConvergerChannel handles the postActivity event: a Direct Line-style activity is stored through Activities.create_client_activity/2 (the pipeline is the only delivery path, ADR-0003), and the reply carries id, seq and watermark. The sender is the token's verified user_id, else from.id, else "user" (protocol v1, section 3.2). An optional clientId (protocol v1 syntax) is stored as ws:<sender>:<clientId>, like the legacy idempotency_key. Sends share the tenant's activity_create rate-limit bucket with REST.
  • Deprecated, removed no earlier than two minor releases and 6 months after this change: /socket (UserSocket, ConversationChannel), POST /api/v1/tokens, and every x-channel-token use (channel tokens on POST /api/v1/conversations and on the tenant API). The tenant API with x-api-key is not deprecated: it is the server-to-server API and has no replacement.
  • ConvergerWeb.Deprecation logs a warning per legacy socket connection and per HTTP request, emits [:converger, :deprecated, :use], and adds the RFC 9745 Deprecation and Link: <guide>; rel="deprecation" headers to HTTP responses.
  • Legacy verifiers check the token's shape: verify_channel_token/1 (has channel_id, no conversation_id) for x-channel-token, verify_conversation_token/1 for the legacy socket, and both reject Converger API tokens (type: "converger").

postActivity is a pre-v1 event on the pre-v1 (activitySet) shape of the topic. Protocol v1 frames (#22) replace it with text and the other message frames plus ack (#24); postActivity follows the deprecation of the activitySet shape.

Consequences​

Positive​

  • New client work (#22 to #28) targets one socket.
  • All WebSocket tests run against the Converger API socket; the legacy tests only pin the deprecation warning and what still ships.
  • Deprecated-surface usage is visible in logs and telemetry.
  • End-user tokens can no longer act as the tenant.

Negative and trade-offs​

  • Integrations that sent a conversation token as x-channel-token to the tenant API break immediately (security fix, not deprecation).
  • REST POST .../activities still takes from.id as the sender even when the token names a user_id; only the socket enforces the verified id.
  • Per-request warnings on the tenant API can be noisy for an integration that still uses a channel token; that is intended pressure to migrate.

Follow-ups​

  • #22: protocol v1 frames on the same socket; postActivity and activitySet become the deprecated pre-v1 shape.
  • #24: ack frames for clientId.
  • Remove the legacy stack after the deprecation window (protocol v1, section 13.3).

Implementation​

  • ConvergerWeb.ConvergerChannel (postActivity).
  • ConvergerWeb.Deprecation, called from UserSocket.connect/3, TenantAuth, TokenController.create/2 and ConversationController.create/2.
  • Converger.Auth.Token (verify_channel_token/1, verify_conversation_token/1).
  • Tests: test/converger_web/channels/converger_channel_test.exs, conversation_channel_test.exs (deprecation), test/converger_web/controllers/legacy_deprecation_test.exs, auth_test.exs (end-user tokens refused), and the integration tests. The chaos harness drives the Converger API socket.