Add area-level documentation for the five most significant cross-context feature areas under agents-docs/features/: - websocket-envelopes: full envelope catalogue, lifecycle, dispatcher - ipc-bridge: window.electronAPI surface, IPC channels, CQRS dispatch - plugin-system: manifest contract, runtime, capabilities, plugin-support API - server-directory: REST endpoints, CQRS, entities, business rules - voice-signaling: mesh signaling, RNNoise pipeline, domain split Update agents-docs/FEATURES.md index alphabetically and remove the "no cross-context feature docs" placeholder. Each doc records honest TODOs for verified gaps (stale signaling-contracts.ts, window.api vs window.electronAPI mismatch, IPC error envelope drift from CONTEXT.md, missing OpenAPI coverage for server-directory routes, no envelope round-trip test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
WebSocket Envelopes
Area: websocket-envelopes Status: Active Last updated: 2026-05-25
Overview
The WebSocket envelope contract is the realtime wire-format boundary between the signaling server and every connected client. Every realtime concern in Toju — presence, chat broadcasts, typing indicators, voice state, WebRTC offer/answer/ICE forwarding, direct messages, server icon P2P sync, and plugin events — travels as a typed envelope over a single WebSocket connection per client. Drift between the server definition and the client-side mirror is treated as a wire-protocol break.
Responsibilities
- Define the canonical shape of every realtime message exchanged between
toju-app(renderer) andserver. - Route incoming envelopes to a single dedicated handler on the server.
- Provide a stable identity for the connection (
connectionId,oderId,connectionScope) and a lazy authorization model onjoin_server. - Forward peer-targeted envelopes (WebRTC signaling, direct messages, server-icon peer transfers) without inspecting their payload.
This area does not own:
- The HTTP/REST surface (see server-directory).
- WebRTC media transport or session orchestration (see voice-signaling — the envelope contract is shared, but session lifecycle lives there).
- Persistence (server entities are owned by the server subdomain; the envelope is the contract, not the entity).
Key concepts
- Envelope — a
{ type, ...payload }message routed bytype. Defined inserver/src/websocket/types.ts. - ConnectedUser — server-side state record per WebSocket:
connectionId,oderId,connectionScope,displayName,description,status,serverIds,lastPong. oderId— opaque user identity. Set by the client inidentify; falls back to a UUID if absent. Multiple connections may share anoderId(e.g. multiple devices) — broadcasts are deduplicated peroderId.connectionScope— typically the signal URL; disambiguates several connections from the sameoderId.- Handler — server-side function mapped to one envelope
typeinserver/src/websocket/handler.ts. - Forwarded envelope — peer-to-peer envelopes the server relays untouched to a specific
targetUserId(offer / answer / ice_candidate / direct-call / direct-message family / server_icon_peer_*).
Envelope catalogue
Defined on the server in server/src/websocket/types.ts and dispatched by the switch in server/src/websocket/handler.ts. Groups below match the dispatch shape, not a literal grouping in code.
Connection & presence
identify— client → server. Profile +connectionScope. Required before any other envelope is meaningful.connected— server → client. Sent automatically on connect:{ connectionId, serverTime }.keepalive— client ↔ server. ResetslastPong. See lifecycle below.status_update— broadcast presence:online | away | busy | offline.access_denied— server → client whenjoin_serverauthorization fails.
Server membership
join_server— client requests membership for aserverId. Authorization checked viaauthorizeWebSocketJoin(server/src/services/server-access.service.ts). Response includesserver_users+plugin_requirements.view_server— client marks a server as viewed (fetch roster + plugin requirements without joining).leave_server— client leaves; broadcastsuser_leftto remaining members.server_users— server → client. Full peer roster for a joined server (used as the seed for P2P offers).user_joined/user_left— broadcast presence changes.
Chat & typing
chat_message— broadcast to a server. Payload:{ message, senderId, senderName, timestamp }.typing— broadcast:{ isTyping, channelId, oderId, displayName }.
Voice presence
voice_state— broadcast user voice state (mute/deafen/room metadata). Pure signaling — the server does not store voice room membership.
WebRTC signaling (forwarded)
offer/answer/ice_candidate— forwarded totargetUserIdviaforwardRtcMessage().direct-call— forwarded; semantic call lifecycle lives in thedirect-callproduct-client domain.
Direct messages (forwarded)
direct-message,direct-message-status,direct-message-mutation,direct-message-sync,direct-message-sync-request— forwarded totargetUserId.
Server icon P2P sync
server_icon_available— client announces it has an icon at versioniconUpdatedAt.server_icon_sync_request— client asks the server which peers have a newer icon.server_icon_sync_peers— server → client. Peer list offering newer icons.server_icon_peer_request/server_icon_peer_data— P2P transfer, forwarded.
Plugins
plugin_event— validated against the plugin's registered event schema (see plugin-system andserver/src/services/plugin-support.service.ts), then broadcast within the server scope. Payload:{ serverId, pluginId, eventName, payload, sourcePluginUserId, sourceUserId, emittedAt }.
Connection lifecycle
Implemented in server/src/websocket/index.ts.
- Client opens WebSocket → server generates
connectionId(UUID), creates theConnectedUserrecord, sends{ type: 'connected', connectionId, serverTime }. - Client sends
identifywithoderId,displayName,connectionScope, optionaldescription/profileUpdatedAt. Server normalizes and stores. - Client sends
join_server(orview_server) per server they care about. Eachjoin_serveris authorized independently. - Heartbeat: server pings every 30 s (
PING_INTERVAL_MS). Any incoming message also refresheslastPong. Connections without a pong for 45 s (PONG_TIMEOUT_MS) are terminated. - On close: server emits
user_leftto every server the connection had joined. Broadcasts are deduplicated byoderId, so multi-device users only generate one departure event per logical identity.
Authentication model
There is no bearer token or signed envelope. Identity is whatever the client claims in identify. Authorization is per-join_server, evaluated by authorizeWebSocketJoin against persisted server access rules (private flag, password hash, bans, invite/join-request state). access_denied is returned when authorization fails; the connection itself stays open.
Technical implementation
Server
- Types:
server/src/websocket/types.ts—WsMessage(union overtype),ConnectedUser,ConnectionScope. - Dispatcher:
server/src/websocket/handler.ts—handleWebSocketMessage(connectionId, message). Single switch (~16 dedicated handler functions plusforwardRtcMessage). - Lifecycle:
server/src/websocket/index.ts—wsserver, ping/pong, connection registry, dead-connection reaping. - Plugin event validation:
server/src/services/plugin-support.service.ts— asyncvalidatePluginEventEnvelope()(runtime schema check).
Client (renderer)
- Shared types:
toju-app/src/app/shared-kernel/signaling-contracts.ts— stale, only declares a genericSignalingMessageand an obsoleteSignalingMessageTypeenum. Not the active wire-format definition. - Active envelope shapes are defined inline as
IncomingSignalingMessageintoju-app/src/app/infrastructure/realtime/signaling/signaling-message-handler.ts. - Constants:
toju-app/src/app/infrastructure/realtime/realtime.constants.ts— every envelopetypestring lives here asSIGNALING_TYPE_*. - Transport:
toju-app/src/app/infrastructure/realtime/signaling/signaling-transport-handler.ts— socket lifecycle, sendsidentify,join_server, raw envelopes. - Coordinator:
toju-app/src/app/infrastructure/realtime/signaling/server-signaling-coordinator.ts— mapsserverIdto signal URL (Toju supports multiple federated signaling endpoints). - Inbound dispatch:
signaling-message-handler.ts—handleConnectedSignalingMessage,handleServerUsersSignalingMessage,handleUserJoinedSignalingMessage,handleUserLeftSignalingMessage,handleOfferSignalingMessage,handleAnswerSignalingMessage,handleIceCandidateSignalingMessage,handleAccessDeniedSignalingMessage. Domain envelopes (chat/typing/direct-message/etc.) are consumed in the respective product-client domains, not in this central adapter — TODO: enumerate exact subscription points.
Versioning
No version field on envelopes. No Accept-Version header. Drift between server and client is enforced only by code review (per server/CONTEXT.md invariants).
Testing
server/src/websocket/handler-status.spec.ts—status_updatebroadcast and profile metadata inuser_joined/server_users.server/src/websocket/handler-plugin.spec.ts—plugin_eventvalidation and broadcast.toju-app/src/app/infrastructure/realtime/signaling/signaling-message-handler.spec.ts— inbound handler unit tests (notablyuser_leftpreserving peers under voice).- TODO: no round-trip envelope-shape test between server
WsMessageand clientIncomingSignalingMessage. Drift can only be caught by E2E or manual review today.
Security considerations
- No transport-level auth — identity is self-asserted via
identify. The server trustsoderIdfor routing but checks authorization on everyjoin_server. - WebRTC signaling envelopes (
offer/answer/ice_candidate) are forwarded without inspection. The server does not verify that the sender is a member of the same server as the target — TODO: confirm whetherforwardRtcMessageenforces server-membership before forwarding. plugin_eventpayloads are bounded by the plugin's declaredmaxPayloadBytes(default 64 KB) and validated against the plugin's declared event schema. See plugin-system.- Multi-connection identities: a single
oderIdmay have many open sockets. Broadcasts dedupe byoderId, but per-connection state (e.g.voice_state) does not — TODO: document the cross-connection invariants.
Performance considerations
- Single WebSocket per client. No fan-out worker; broadcast is in-process via the in-memory connection map.
- Ping cadence 30 s / pong timeout 45 s. Reaping is per-connection on next tick.
- TODO: no documented soft cap on connected users per signaling server.
Known issues and limitations
- Stale shared-kernel contract.
toju-app/src/app/shared-kernel/signaling-contracts.tsdoes not enumerate the live envelope set; client code usesIncomingSignalingMessageinsignaling-message-handler.tsinstead. Update or replace this file when adjacent work touches the wire format. - No envelope versioning. Any field rename is an immediate break for older clients.
- TODO — operator concerns: rate limits, max-message-size, and backpressure are not documented.
Related features
- voice-signaling — consumes
offer/answer/ice_candidate/voice_state/direct-call. - plugin-system — defines and validates
plugin_event. - server-directory — REST counterpart for server discovery, joining, and moderation;
join_serverenvelope authorization reuses the same access rules.
Changelog
| Date | Change |
|---|---|
| 2026-05-25 | Initial documentation |