Room Events¶
Room events let a connection join and leave the named fan-out channels the server publishes to. Joining a room means the server delivers subsequent room broadcasts to the connection; leaving stops them. Room membership is per connection, cleared automatically when the connection closes.
Reference: rooms.md · events/auction.md
Event list¶
| Event | Direction | Status |
|---|---|---|
room.join | Client → Server | Implemented |
room.leave | Client → Server | Implemented |
There are no server-pushed room.* events. Room membership itself is never announced; the events flowing through a room are the domain events of the rooms that were joined (e.g. auction.* on auction:<auctionId>, company.* on company:<companyId>).
Room names and access¶
A room.join request is honored only when the room name matches the caller's authorized scope. room.leave does not re-validate the room name (leaving is idempotent).
| Room pattern | Who may join |
|---|---|
notifications | Any authenticated connection. |
admins | SUPERADMIN only. |
user:<id> | Only when <id> equals the authenticated user's id. |
company:<id> | Only when <id> equals the company user's resolved companyId (canonical Company.id). |
auction:<id> | Forbidden via room.join. Rejected with 403 Forbidden ("Use auction.subscribe..."). |
auction:staff:<id> | Forbidden via room.join. Auto-joined via auction.subscribe for authorized staff. |
| anything else | Rejected with 403 Forbidden ("You are not authorized to join this room."). |
[!WARNING] Attempting to join any auction room via
room.join(auction:<auctionId>orauction:staff:<auctionId>) is explicitly rejected with 403 Forbidden ("Use auction.subscribe to join an authorized auction channel."). Clients must send the typedauction.subscribemessage instead.
auction.subscribe authorizes the caller against the specific auction (requiring auction.read at program scope for company staff), auto-joins staff to auction:staff:<auctionId>, registers presence, and sends auction.connected, auction.state.snapshot, and auction.audience.snapshot.
room.join¶
Description¶
Requests membership in a named room for the current connection. The server validates the room name against the caller's scope, joins the connection to the room, and ensures the per-room Redis subscription (so broadcasts reach the connection even across WebSocket server instances). Joining an already-joined room is a harmless no-op.
Direction¶
Client → Server
Roles Allowed¶
SUBSCRIBER, COMPANY, SUPERADMIN — any authenticated connection holding the room:join permission. The room name determines which rooms are actually reachable (see Room names and access).
Permissions Required¶
room:join.
Request Schema¶
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | room.join. |
id | string | No | Client-generated id, echoed in the ack. 1–128 chars. |
data.room | string | Yes | Room name to join. Trimmed, 1–160 characters. |
Response Schema¶
| Field | Type | Description |
|---|---|---|
data.eventType | string | Always "room.join". |
data.room | string | The room that was joined. |
data.joined | boolean | Always true. |
Success Response¶
{
"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
}
}
Error Responses¶
| Code | Description |
|---|---|
BAD_REQUEST | room missing, empty, or longer than 160 characters; unknown type. |
UNAUTHORIZED | Connection is not authenticated (handshake-level). |
FORBIDDEN | Missing room:join permission, attempting to join auction:*, or the room is outside caller scope. |
RATE_LIMITED | Message rate limit exceeded. |
Validation Rules¶
| Rule | Behaviour |
|---|---|
room non-empty string (max 160 chars) | Required; otherwise BAD_REQUEST. |
| Room name in caller scope | Enforced via Room names and access; otherwise FORBIDDEN. |
Example Request¶
{
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"type": "room.join",
"data": {
"room": "user:sub_01HQ5BWYP1Y5RX0W5X0W5X0W5X"
}
}
Broadcast Behaviour¶
The ack is unicast to the requesting connection. Joining itself is not announced to other members; the server simply starts delivering the room's future broadcasts to the connection.
Related Events¶
room.leaveauction.subscribe,auction.unsubscribe— the mandatory, auction-authorized way to enter/leave auction channels.auction.audience.changed— emitted for distinct audience transitions.
Notes¶
- Implementation status: Implemented (
room.ts). - Joining is idempotent; re-joining an already-joined room succeeds.
- The server ensures the per-room Redis subscription on join and drops it once the last member leaves.
Best Practices¶
- Always use
auction.subscribefor auction rooms; directroom.joinforauction:*returns403 Forbidden. - Join
company:<companyId>once to receive company-scoped broadcasts. - Join
user:<id>for private, user-targeted messages. - Track the rooms you joined and re-join them after a reconnect.
room.leave¶
Description¶
Removes the current connection's membership from a named room. The server does not re-validate room-name scope on leave; leaving a room the connection never joined still succeeds. When the last member of a room leaves on a process, the per-room Redis subscription is released (reference-counted).
Direction¶
Client → Server
Roles Allowed¶
SUBSCRIBER, COMPANY, SUPERADMIN — any authenticated connection holding the room:leave permission.
Permissions Required¶
room:leave.
Request Schema¶
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | room.leave. |
id | string | No | Client-generated id, echoed in the ack. 1–128 chars. |
data.room | string | Yes | Room name to leave. Trimmed, 1–160 characters. |
Response Schema¶
| Field | Type | Description |
|---|---|---|
data.eventType | string | Always "room.leave". |
data.room | string | The room that was left. |
data.left | boolean | Always true (idempotent). |
Success Response¶
{
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"type": "ack",
"version": "1.0",
"source": "dichit-backend",
"timestamp": "2026-08-05T10:00:00.000Z",
"sequence": 2,
"data": {
"eventType": "room.leave",
"room": "auction:cyc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"left": true
}
}
Error Responses¶
| Code | Description |
|---|---|
BAD_REQUEST | room missing, empty, or longer than 160 characters; unknown type. |
UNAUTHORIZED | Connection is not authenticated (handshake-level). |
FORBIDDEN | Missing room:leave permission. |
RATE_LIMITED | Message rate limit exceeded. |
Validation Rules¶
| Rule | Behaviour |
|---|---|
room non-empty string (max 160 chars) | Required; otherwise BAD_REQUEST. |
| Room scope on leave | Not re-validated; leaving is idempotent. |
Example Request¶
{
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"type": "room.leave",
"data": {
"room": "company:com_01HQ5BWYP1Y5RX0W5X0W5X0W5X"
}
}
Broadcast Behaviour¶
The ack is unicast to the requesting connection. Leaving is not announced to the remaining members. Leaving an auction:<auctionId> room stops feed delivery but does not end the subscriber's presence: presence is tied to auction.subscribe / auction.unsubscribe, so auction.audience.changed is only published when the connection's last auction subscription ends, not on a raw room.leave.
Related Events¶
room.joinauction.unsubscribe— leaves the auction room and clears presence.
Notes¶
- Implementation status: Implemented (
room.ts). - Idempotent: leaving a room that was never joined returns
left: true. - On disconnect, all memberships for the connection are removed automatically and the Redis subscription is dropped once the room has no members anywhere.
Best Practices¶
- Leave generic rooms (
company:<id>,user:<id>,notifications) as soon as they are no longer needed to avoid wasted delivery. - Use
auction.unsubscribeto leave an auction room so presence is cleared. - Rely on connection close to clean up any rooms you forgot to leave.
Related documents¶
- rooms.md — room architecture, membership rules, and delivery guarantees.
- events/auction.md —
auction.subscribe/auction.unsubscribe, the auction-aware room flow. - events/presence.md — how presence is tied to subscribe/unsubscribe, not raw room membership.
- errors.md — error codes referenced above.