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:
Events are forwarded verbatim from NATS as events.Envelope:
Errors:
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 |