Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/artemis-development-group/artemis/llms.txt

Use this file to discover all available pages before exploring further.

The Artemis WebSocket service broadcasts real-time messages from the server to connected browser clients. The main application publishes messages through an AMQP fan-out exchange (RabbitMQ), and the WebSocket service — a separate process — delivers those messages to the appropriate connected clients. This page covers the architecture, server-side publishing API, client connection model, and how to run the service locally.

Architecture overview

  Artemis main app

       │  amqp.add_item(routing_key=namespace, exchange="sutro")

  RabbitMQ (sutro fan-out exchange)

       │  every worker process binds an exclusive queue to the exchange

  artemis-service-websockets

       │  maps routing_key → socket namespace

  Browser clients (WebSocket connection)
The main application and the WebSocket service communicate only through AMQP. The WebSocket service never reads from the main application’s database.
Each worker process in the WebSocket service binds its own exclusive, auto-delete queue to the fan-out exchange, so every worker receives every message and can dispatch to any client it holds.

Server-side: publishing messages

Use r2.lib.websockets in the main application to send a message to all clients subscribed to a namespace.
r2/r2/lib/websockets.py
def send_broadcast(namespace, type, payload):
    """Broadcast an object to all WebSocket listeners in a namespace."""
    frame = {
        "type": type,
        "payload": payload,
    }
    amqp.add_item(
        routing_key=namespace,
        body=json.dumps(frame),
        exchange=_WEBSOCKET_EXCHANGE,   # "sutro"
    )
Parameters:
ParameterTypeDescription
namespacestrRouting key and WebSocket path. Clients connected to this namespace receive the message.
typestrIdentifies the kind of payload so client-side code can route it.
payloaddictArbitrary JSON-serialisable data to send to clients.
Example:
from r2.lib.websockets import send_broadcast

send_broadcast(
    namespace="/live/t3_abc123",
    type="new_comment",
    payload={"comment_id": "t1_xyz", "body_html": "<p>Hello</p>"},
)

Client connection: signed URLs

Clients do not connect to an open WebSocket endpoint. The main application generates a time-limited, signed URL that the browser uses to establish the connection.
r2/r2/lib/websockets.py
def make_url(namespace, max_age):
    """Return a signed URL for the client to use for websockets."""
    signer = MessageSigner(g.secrets["websocket"])
    signature = signer.make_signature(
        namespace, max_age=datetime.timedelta(seconds=max_age)
    )

    query_string = urllib.urlencode({"m": signature})

    return urlparse.urlunparse(
        ("wss", g.websocket_host, namespace, None, query_string, None)
    )
Parameters:
ParameterTypeDescription
namespacestrThe path the client will subscribe to (e.g. /live/t3_abc123).
max_ageintSeconds the signed URL remains valid.
The returned URL has the form:
wss://<websocket_host><namespace>?m=<signature>
The browser passes this URL directly to the WebSocket constructor. The WebSocket service validates the m query parameter against the shared secret before accepting the connection.
The signing secret (g.secrets["websocket"]) must match the secret/websockets/authorization_key value in the WebSocket service’s secrets file. A mismatch will cause all connections to be rejected.

Namespaces

The path portion of the WebSocket URL is the namespace. It serves two purposes:
  1. Authorization scope — The signature binds to this exact path, so a token for /live/t3_abc123 cannot be used to connect to /live/t3_xyz789.
  2. Message routing — The WebSocket service maps the AMQP routing key to the socket namespace. A call to send_broadcast(namespace="/live/t3_abc123", ...) reaches only clients connected to that path.

Configuration

example.ini

The WebSocket service is configured through an ini file. The key sections are:
example.ini
[app:main]
factory = artemis_service_websockets.app:make_app

; AMQP broker connection
amqp.endpoint = rabbit.local:5672
amqp.vhost = /
amqp.username = guest
amqp.password = guest

; fan-out exchange the main app writes to
amqp.exchange.broadcast = sutro

; topic exchange for connect/disconnect status events
amqp.exchange.status = artemis_exchange

; set true to publish connect/disconnect events to the status exchange
amqp.send_status_messages = false

; seconds between unsolicited PING frames (Firefox requires activity < 55s)
web.ping_interval = 45

; base64-encoded token for admin-only service endpoints
web.admin_auth = aHVudGVyMg==

; connections per second to shed when the service is quiescing
web.conn_shed_rate = 5

secrets.path = example_secrets.json

[server:main]
factory = baseplate.server.wsgi
handler = artemis_service_websockets.socketserver:WebSocketHandler

example_secrets.json

The secrets file holds the shared signing key. It must use the versioned type so the service can rotate keys without dropping connections:
example_secrets.json
{
    "secrets": {
        "secret/websockets/authorization_key": {
            "type": "versioned",
            "current": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5",
            "encoding": "base64"
        }
    },
    "vault_token": "17213328-36d4-11e7-8459-525400f56d04"
}
In production, replace current with a randomly generated 32-byte value encoded as base64. The vault_token field is used when the secrets store is backed by HashiCorp Vault.

Connect/disconnect notifications

When amqp.send_status_messages = true, the service publishes events to the topic exchange (amqp.exchange.status) whenever a client connects or disconnects. The routing key format is:
websocket.<event>.<namespace_path>
Backend consumers (queue workers) can bind to this exchange to react to client presence changes — for example, to trigger a cache warm-up when a user connects to a live thread.

Docker development setup

The websockets/ directory ships two Dockerfiles for local development and testing.
1

Build and run the WebSocket service

# Exposes the service at 127.0.0.1:9090
docker build . -t ws-server -f Dockerfile \
  && docker run --rm -p 9090:9090 ws-server
The service will be available at ws://127.0.0.1:9090.
2

Run the test suite

docker build . -t ws-tests -f Dockerfile.test \
  && docker run ws-tests
3

Point the main app at the local service

In the main application’s ini file, set:
websocket_host = 127.0.0.1:9090
Signed URLs generated by make_url() will now point to the local service.

Plugin system

Use the plugin system and HookRegistrar to fire WebSocket broadcasts in response to platform events.

AMQP queue workers

Declare AMQP queues from your plugin with declare_queues() to process messages produced by the WebSocket service’s status exchange.

Build docs developers (and LLMs) love