Skip to content

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> or auction:staff:<auctionId>) is explicitly rejected with 403 Forbidden ("Use auction.subscribe to join an authorized auction channel."). Clients must send the typed auction.subscribe message 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.

  • room.leave
  • auction.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.subscribe for auction rooms; direct room.join for auction:* returns 403 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.

  • room.join
  • auction.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.unsubscribe to leave an auction room so presence is cleared.
  • Rely on connection close to clean up any rooms you forgot to leave.

  • 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.