Skip to content

Rooms

A room is a named fan-out channel that distributes messages to the connections currently joined to it. Rooms let the server publish an update exactly once and have it delivered to every interested connection, including connections hosted by different WebSocket server instances.

Reference: protocol.md · connection.md · diagrams.md


Room architecture

  • Rooms are named, e.g. auction:cyc_01..., company:com_01....
  • A connection may belong to many rooms at once.
  • Room membership is per connection and stored in-memory per process.
  • Membership is cleared automatically when a connection closes.
  • Delivery across multiple server instances is handled by Redis Pub/Sub; each server that has members in a room maintains a Redis subscription for that room.
  • A client that does not hold the right permission cannot join a room; a join that fails permission checks returns FORBIDDEN.

Room channels

Concept Value Description
Local room name auction:<cycleId> Name used in room.join / room.leave.
Redis subscription channel ws:room:<roomName> Per-room Redis channel for cross-instance delivery.
Global channel ws:global Broadcast to every connection on every instance.

The auction.subscribe event joins the auction:<cycleId> room and ensures the server subscribes to ws:room:auction:<cycleId> automatically. Ordinary room.join also calls the same subscription ensure.

Room reference

auction:<cycleId>

Attribute Value
Purpose Delivers live auction state, bids, and presence transitions.
Who joins Authenticated connections via room.join, or automatically via auction.subscribe.
Who publishes Auction engine, bid commands, presence service (server-side).
Who receives Every connection currently joined to the room, across all servers.
Lifetime Created on first subscriber and subsisting Redis subscription; room membership persists per-connection until the connection closes or the client leaves/unsubscribes.

Example join:

{
  "type": "room.join",
  "requestId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "data": { "room": "auction:cyc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X" }
}
{
  "type": "ack",
  "requestId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "eventType": "room.join",
  "data": { "room": "auction:cyc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X", "joined": true }
}

Prefer auction.subscribe to entering auction rooms; it enforces full access authorization and enables snapshot/replay and presence registration that a raw room.join does not.

company:<companyId>

| Purpose | Broadcasts company-scoped staff updates (e.g. auction events concerning the company) to all of the company's connections. | | Who joins | COMPANY (and SUPERADMIN where applicable) connections for their own company id. | | Who publishes | Company/auction staff workflows. | | Who receives | All company connections in the room, across servers. | | Lifetime | Per-connection; removed on disconnect. |

The server only permits a connection to join company: for its own companyId. room.join enforces this.

user:<userId>

| Purpose | Private updates targeted at a single user (e.g. direct notifications). | | Who joins | The user's own connections only (user:<userId> where userId is the authenticated user id). | | Who publishes | Server business logic and background jobs. | | Who receives | One or more connections belonging to that user. | | Lifetime | Per-connection; removed on disconnect. |

Example: a connection can join its own private room:

{
  "type": "room.join",
  "requestId": "req_01HQ5G2Y5Y1P5RX0W5X0W5X0W5X",
  "data": { "room": "user:sub_01HQ5BWYP1Y5RX0W5X0W5X0W5X" }
}

admins

| Purpose | Platform-wide administrative broadcasts (e.g. system alerts, cross-company announcements). | | Who joins | SUPERADMIN only. | | Who publishes | Platform administrator/security logic. | | Who receives | Connected SUPERADMIN sockets. | | Lifetime | Per-connection; removed on disconnect. |

A non-admin attempting to join admins is rejected:

{
  "type": "error",
  "requestId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "code": "FORBIDDEN",
  "message": "Only admins can join the admins room."
}

notifications

| Purpose | Per-user notification delivery. The room name is the same for every user; the server scopes delivery by user routing rather than by per-user rooms when needed. | | Who joins | any authenticated connection. | | Who publishes | Notification engine. | | Who receives | all members. Usually each user connects and receives their own notifications via user-scoped routing. | | Lifetime | Per-connection; removed on disconnect. |

Room access rules

A room.join request is only honored if the room name matches the caller's scope. The rules are:

Room pattern Allowed for
notifications any authenticated connection
admins SUPERADMIN
user:<id> only when <id> equals the current user's id
company:<id> only when <id> equals the current user's companyId
auction:<id> any authenticated connection with room:join

Any other room name is rejected with Errors FORBIDDEN ("You are not authorized to join this room.").

Note: auction:<id> room membership grants membership only. The full auction data stream requires auction.subscribe (which authorizes the caller against the specific auction and registers presence).

Member issuing and leaving

  • room.leave removes the connection from the room (idempotent; leaving a room that wasn't joined still returns left: true).
  • When the last connection leaves a room, the process unsubscribes the Redis channel it no longer needs (the Redis subscription is reference-counted).
  • On disconnect, all room memberships for the connection are removed and the Redis subscription is dropped once the room has no members anywhere.

Example leave:

{
  "type": "room.leave",
  "requestId": "req_01HQ5BXWYP1Y1RX0W5X0W5X0W5X",
  "data": { "room": "auction:cyc_01HQ5BXWYP1Y1RX0W5X0W5X0W5X" }
}
{
  "type": "ack",
  "requestId": "req_01HQ5BXWYP1Y1RX1W5X0W5X0W5X",
  "eventType": "room.leave",
  "data": { "room": "auction:cyc_01HQ5BXWYP1Y1RX0W5X0W5X0W5X", "left": true }
}

Delivery guarantees

  • Room broadcasts are at-most once; clients must be idempotent and use serverSequence / snapshots to reconcile.
  • A server-side requestId echoes to the client when the broadcast carries one.
  • Ordering is preserved within a room channel on a given process; across instances it is near‑real‑time but not strictly total.

Room examples

For end-to-end examples of subscribing, receiving bids, and presence in a room, see: