Skip to content

WebSocket Gateway

Partly out of date

Rendered from ProjectInfo/server/gateway.md on docs/encryption-modes-spec. Its protocol content is still useful, but parts of it describe an older client or an earlier API. Where it disagrees with the code, the code wins — see the spec errata.

Implementation: services/api/internal/gateway/

Components

Hub (hub.go)

The Hub manages all active WebSocket clients and their NATS subscriptions. It is safe for concurrent use.

type Hub struct {
    nc     *nats.Conn
    logger *zap.Logger
    mu     sync.Mutex
    subs   map[string]*subState  // subject prefix → { clients, natsSub }
}

Subscription model: NATS subscriptions are created lazily per subject prefix (channel.<id>, server.<id>, user.<id>). The subscription pattern is <prefix>.*, which matches all event types for that prefix. When the last WebSocket client unsubscribes from a prefix, the NATS subscription is torn down.

Fan-out: On receiving a NATS message, hub.deliver takes a snapshot of the client set under the lock, releases it, then sends to each client's buffered channel. If a client's send buffer (sendBufSize = 256) is full, the message is dropped with a warning log.

Client (client.go)

One Client per active WebSocket connection. Runs two goroutines:

readPump — reads inbound frames from the WebSocket: - Sets read deadline to 60s (pongWait); deadline is refreshed on each Pong frame - Max frame size: 4096 bytes - Parses InboundMessage and dispatches to handler logic - Cleans up by calling hub.UnsubscribeAll(c) on exit

writePump — drains the send channel and writes to WebSocket: - Sends a ping every (60 * 9 / 10) seconds = 54 seconds - Sets write deadline to 10s per frame - Exits if ping fails (peer considered dead)

type Client struct {
    hub    *Hub
    conn   *websocket.Conn
    send   chan []byte    // buffered, cap 256
    userID string
    logger *zap.Logger
}

Handler (handler.go)

Upgrades the HTTP connection to WebSocket, authenticates via the JWT in ?token=, creates a Client, starts the pump goroutines, and subscribes the client to the user's own user.<userID> subject.

Inbound Messages

Clients send JSON frames with an op field:

type InboundMessage struct {
    Op         string   `json:"op"`
    ChannelIDs []string `json:"channel_ids,omitempty"`
    ServerIDs  []string `json:"server_ids,omitempty"`
    ChannelID  string   `json:"channel_id,omitempty"`
}
op Action
subscribe hub.SubscribeChannels(c, channelIDs) + hub.SubscribeServers(c, serverIDs)
unsubscribe hub.UnsubscribeChannels(c, channelIDs)
typing Publish TYPING_START event to channel.<channelID> NATS subject
heartbeat Respond with { "op": "heartbeat_ack" }

Outbound Messages

All outbound frames are JSON. On connect, the server immediately sends:

{ "op": "hello" }

Events are forwarded verbatim from NATS as events.Envelope:

{ "op": "event", "type": "MESSAGE_CREATE", "data": { ... } }

Errors:

{ "op": "error", "message": "invalid message format" }

Connection Lifecycle

HTTP GET /gateway?token=<jwt>
  → Upgrade to WebSocket
  → Authenticate JWT
  → Create Client
  → Send { "op": "hello" }
  → Start readPump + writePump goroutines
  → hub.SubscribeUser(client)  ← auto-subscribes to user.<id>.*

Client: { "op": "subscribe", "channel_ids": [...], "server_ids": [...] }
  → hub.SubscribeChannels + hub.SubscribeServers

Client: { "op": "heartbeat" } every 30s
  → { "op": "heartbeat_ack" }

Client disconnects (or no heartbeat for 60s)
  → readPump returns
  → hub.UnsubscribeAll(client)
  → conn.Close()

Rate Limits

WebSocket connect attempts: 5 connections per 30 seconds per IP (GatewayConnectLimit), applied before the Authenticate middleware. Prevents connection floods from unauthenticated attackers.

Publishing Events

All domain services publish events via the NATS connection injected at startup:

// events.Publish helper
events.Publish(nc.Publish, events.ChannelSubject(channelID, events.TypeMessageCreate), payload)

This marshals events.Envelope{Op: "event", Type: ..., Data: payload} and publishes the JSON bytes to NATS. The Hub receives it and fans it out to all subscribed WebSocket clients.

NATS Subject Reference

Subject Triggered by
channel.<id>.MESSAGE_CREATE message.Service.Send
channel.<id>.MESSAGE_UPDATE message.Service.Edit
channel.<id>.MESSAGE_DELETE message.Service.Delete
channel.<id>.TYPING_START gateway inbound typing op
server.<id>.MEMBER_JOIN server.Service.JoinServer
server.<id>.MEMBER_LEAVE server.Service.LeaveServer / KickMember
server.<id>.ROLE_UPDATE server.Service.UpdateRole / AssignRole
user.<id>.PRESENCE_UPDATE user.Service.UpdatePresence
user.<id>.USER_UPDATE user.Service.UpdateProfile
user.<id>.DM_OPEN dm.Service.OpenChannel