WebSocket proxy for SimpleX Chat CLI — multiplex one upstream connection to N downstream clients with transparent corrId routing.
Architecture · Features · How it works · Installation · Configuration · Usage · Development · License
flowchart TB
subgraph Downstream["Downstream clients"]
C1[Client 1]
C2[Client 2]
C3[Client N]
end
subgraph Proxy["simplex-ws-proxy :6225"]
direction TB
REG[ClientRegistry]
RT[RoutingTable]
Q1[Queue 1]
Q2[Queue 2]
Q3[Queue N]
end
C1 -->|cmd + corrId| Proxy
C2 -->|cmd + corrId| Proxy
C3 -->|cmd + corrId| Proxy
Proxy -->|rewritten corrId| S[SimpleX CLI :5225]
S -->|response / event| Proxy
Proxy -->|routed response| C1
Proxy -->|broadcast event| C2
Proxy -->|broadcast event| C3
style Proxy fill:#4a9eff,stroke:#2277cc,color:#fff
style S fill:#222,stroke:#444,color:#fff
The proxy maintains exactly one upstream WebSocket connection to SimpleX CLI. Each downstream client gets its own message queue and corrId namespace — responses are routed back to the originating client, events are broadcast to everyone.
| Feature | Description |
|---|---|
| 1-to-N multiplexing | Any number of clients share a single upstream SimpleX CLI connection |
| Transparent corrId routing | Commands from each client are tagged with a proxy-level corrId; responses are demultiplexed and delivered only to the originating client |
| Event broadcast | Unsolicited events (no corrId) are forwarded to all connected clients |
| Response broadcast opt-in | Clients connecting with ?broadcast_responses=true receive responses routed to any other connected client, in addition to their own |
| Binary passthrough | Binary (bytes) messages are forwarded transparently between upstream and downstream without any transformation |
| Initial state replay | New clients immediately receive the last cached upstream state on connect |
| Upstream reconnect | Exponential backoff reconnection (1 → 2 → 4 → 8 → 16 → 30 s) with proxyEvent notification to all clients on disconnect |
| Health check endpoint | Built-in HTTP GET /health endpoint returns 200 OK — compatible with Docker HEALTHCHECK and orchestrator liveness probes |
| Bounded queues | Per-client queues (max 1 000 messages) drop oldest messages for slow clients — no head-of-line blocking |
| Stale entry cleanup | Routing table entries expire after a configurable timeout to prevent memory leaks |
| JSON error handling | Invalid JSON from a client gets a structured error response instead of crashing the connection |
| Graceful shutdown | SIGINT / SIGTERM handled cleanly |
| Structured logging | Color-coded, leveled logging via coloredlogs |
SimpleX CLI speaks a simple JSON protocol over WebSocket. Each
command carries a corrId field; the response echoes the same
corrId back so the caller can match request to reply.
The problem: a single WebSocket connection means a single corrId
namespace. Two clients sending {"corrId": "1", ...} simultaneously
would collide.
simplex-ws-proxy solves this by rewriting corrId on the way
out:
Client A sends: {"corrId": "1", "cmd": "/contacts"}
Proxy forwards: {"corrId": "proxy_<clientA-uuid>_1", "cmd": "/contacts"}
SimpleX responds: {"corrId": "proxy_<clientA-uuid>_1", "resp": {...}}
Proxy delivers: {"corrId": "1", "resp": {...}} → Client A only
Events without a corrId (e.g. incoming messages) are broadcast to
all clients unchanged.
When the upstream SimpleX CLI connection drops, every connected client receives a synthetic event:
{
"corrId": "",
"resp": {
"type": "proxyEvent",
"event": "upstreamDisconnected"
}
}
The proxy then reconnects automatically with exponential backoff.
docker run -d \
-p 6225:6225 \
-e SIMPLEX_WS_URL=ws://simplex-cli:5225 \
obeoneorg/simplex-ws-proxy
Or with Docker Compose:
cp docker-compose.example.yml docker-compose.yml
# Edit docker-compose.yml to match your setup
docker compose up -d
The Docker image includes a built-in HEALTHCHECK that polls the
/health endpoint every 30 s.
git clone https://github.com/obeone/simplex-ws-proxy.git
cd simplex-ws-proxy
uv venv && source .venv/bin/activate
uv pip install -e .
Settings can be provided as CLI flags or environment variables. CLI flags take precedence.
| CLI flag | Env variable | Default | Description |
|---|---|---|---|
--upstream | SIMPLEX_WS_URL | ws://localhost:5225 | WebSocket URL of the upstream SimpleX CLI |
--host | PROXY_HOST | 0.0.0.0 | Host address to listen on |
--port | PROXY_PORT | 6225 | Port to listen on |
--stale-timeout | STALE_TIMEOUT | 60 | Seconds before a routing table entry expires |
--log-level | LOG_LEVEL | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
# With defaults (connects to ws://localhost:5225, listens on :6225)
simplex-ws-proxy
# Using CLI flags
simplex-ws-proxy --upstream ws://simplex:5225 --log-level DEBUG
# Using environment variables
SIMPLEX_WS_URL=ws://simplex:5225 LOG_LEVEL=DEBUG simplex-ws-proxy
import asyncio
import websockets
async def main():
async with websockets.connect("ws://localhost:6225") as ws:
await ws.send('{"corrId": "abc123", "cmd": "/contacts"}')
async for message in ws:
print(message) # routed responses + broadcast events
asyncio.run(main())
By default each client's command responses are private — only the
originating client receives them. A client can opt into receiving
responses routed to any other connected client by appending
?broadcast_responses=true to its connection URL:
async with websockets.connect(
"ws://localhost:6225?broadcast_responses=true"
) as ws:
...
When this flag is set, the proxy forwards to this client every
response it routes to any other client, in addition to the client's
own responses. Unsolicited events (messages without a corrId) are
always broadcast to everyone regardless of this setting.
Typical use-case: a monitoring or logging client that wants to observe all command traffic on a shared proxy without being the one issuing the commands.
The proxy exposes an HTTP GET /health endpoint on the same port as
the WebSocket server. It returns 200 OK when the proxy is running:
curl http://localhost:6225/health
A standalone healthcheck.py script is bundled in the Docker image
for use as a HEALTHCHECK command. It reads the PROXY_PORT
environment variable to find the correct port.
| Command | Description |
|---|---|
uv pip install -e ".[dev]" | Install with dev dependencies |
pytest | Run tests |
pytest --cov | Run tests with coverage |
ruff check . | Lint |
ruff format . | Format |
MIT — see LICENSE for details.
Made by obeone
Content type
Image
Digest
sha256:758caf656…
Size
46.3 MB
Last updated
22 days ago
docker pull obeoneorg/simplex-ws-proxy