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:auc_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:<auctionId> | Joined exclusively via auction.subscribe. |
| 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:<auctionId> room and ensures the server subscribes to ws:room:auction:<auctionId> automatically.
[!WARNING] Generic room join commands (
room.join) targeting any room starting withauction:(auction:<auctionId>orauction:staff:<auctionId>) are forbidden and rejected with403 Forbidden("Use auction.subscribe to join an authorized auction channel."). Clients must use the typedauction.subscribecommand instead.Rooms are keyed by
auctionId. The auction row is guaranteed beforeREADY/LIVE(ensureAuctionsForEligibleCycles), soauctionIdalways exists. Fetch it via REST (e.g.GET /v2/.../auctions/by-cycle/:cycleId) before subscribing.
Room reference¶
auction:<auctionId>¶
| Attribute | Value |
|---|---|
| Purpose | Delivers live auction state, bids, and presence transitions. |
| Who joins | Authenticated connections via auction.subscribe only (direct room.join is rejected). |
| 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. |
auction:staff:<auctionId>¶
| Attribute | Value |
|---|---|
| Purpose | Delivers staff-only auction events (events published with scope: "staff") that subscribers must never see. |
| Who joins | COMPANY and SUPERADMIN connections auto-joined during auction.subscribe (direct room.join is rejected). |
| Who publishes | Auction engine / staff workflows publishing staff-scoped realtime events (server-side). |
| Who receives | Every staff connection in the room, across all servers. |
| Lifetime | Created on first staff subscriber and subsisting Redis subscription; membership persists per-connection until the connection closes or the client leaves/unsubscribes. |
Direct room.join to auction:staff:<auctionId> is rejected with FORBIDDEN. Because the room is joined automatically as part of auction.subscribe for staff roles with auction.read permission, a staff client only needs the normal auction.subscribe flow to begin receiving the staff channel.
Example room join for allowed rooms (e.g., company:<companyId>):
{
"type": "room.join",
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"data": { "room": "company:cmp_cm7a1b2c3d4e5f6g7h8i9j00" }
}
{
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"type": "ack",
"version": "1.0",
"source": "dichit-backend",
"timestamp": "2026-08-05T10:00:00.000Z",
"sequence": 1,
"data": {
"eventType": "room.join",
"room": "company:cmp_cm7a1b2c3d4e5f6g7h8i9j00",
"joined": true
}
}
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",
"id": "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:
{
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"type": "error",
"version": "1.0",
"source": "dichit-backend",
"timestamp": "2026-08-05T10:00:00.000Z",
"sequence": 2,
"data": {
"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 authorized scope. The rules are:
| Room pattern | Allowed for |
|---|---|
notifications | any authenticated connection |
admins | SUPERADMIN only |
user:<id> | only when <id> equals the current user's id |
company:<id> | only when <id> equals the company user's resolved companyId (canonical Company.id from membership) |
auction:<id> | Forbidden via room.join; must join via auction.subscribe |
auction:staff:<id> | Forbidden via room.join; must join via auction.subscribe |
Any room.join request for an auction channel (room.startsWith('auction:')) is rejected with 403 Forbidden ("Use auction.subscribe to join an authorized auction channel.").
Any other unlisted room name is rejected with 403 Forbidden ("You are not authorized to join this room.").
[!NOTE]
auction.subscribeis the only channel authorization mechanism for auctions. It checks permission (auction.readat program scope for staff), auto-joins bothauction:<auctionId>andauction:staff:<auctionId>(for staff), replays state, and registers presence. Rawroom.joinis rejected.
Member issuing and leaving¶
room.leaveremoves the connection from the room (idempotent; leaving a room that wasn't joined still returnsleft: 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",
"id": "req_01HQ5BXWYP1Y1RX0W5X0W5X0W5X",
"data": { "room": "auction:auc_01HQ5BXWYP1Y1RX0W5X0W5X0W5X" }
}
{
"id": "req_01HQ5BXWYP1Y1RX1W5X0W5X0W5X",
"type": "ack",
"version": "1.0",
"source": "dichit-backend",
"timestamp": "2026-08-05T10:00:00.000Z",
"sequence": 1,
"data": {
"eventType": "room.leave",
"room": "auction:auc_01HQ5BXWYP1Y1RX0W5X0W5X0W5X",
"left": true
}
}
Delivery guarantees¶
- Room broadcasts are
at-mostonce; clients must be idempotent and useserverSequence/ snapshots to reconcile. - A server-side
idechoes 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:
- events/room.md —
room.join/room.leaverequest and response. - events/auction.md —
auction.subscribe, server events. - events/presence.md — presence transitions in a room.
- diagrams.md — room membership and Redis fan-out diagrams.