Skip to content

WebSocket Protocol

This document defines the Dichit WebSocket protocol: the framing, the message envelope, the request/response model, and naming conventions. It is the contract both clients and servers implement.

Reference: README.md · connection.md · authentication.md · rooms.md · errors.md


WebSocket protocol

  • Transport: WebSocket (RFC 6455).
  • All messages are JSON-encoded text frames. Binary frames are rejected.
  • Endpoint: wss://api.example.com/ws (see connection.md).
  • A connection requires a valid access token at handshake (see authentication.md).
  • The server maintains the connection with protocol-level ping/pong heartbeats.

Connection semantics follow RFC 6455. A single connection is a full-duplex channel: the client sends request messages, the server replies with acknowledgements or errors, and both directions may carry events.

Connection lifecycle

The protocol assumes a stateful lifecycle per connection:

  1. Handshake — the client opens a WebSocket and authenticates with a JWT.
  2. Ready — the connection is established and can send/receive messages.
  3. Subscription — the client joins rooms and/or subscribes to auctions.
  4. Active — the client sends commands and receives pushed updates.
  5. Closed — the connection ends (client or server initiated), heartbeats stop, and all room memberships are released.

See lifecycle.md for the detailed state machine and sequence diagrams.

Request/Response model

The protocol is a hybrid:

  • Request/response: most client messages are commands. The server processes each command and replies with either an acknowledgement (type: "ack") or an error (type: "error"). Correlation uses requestId.
  • Server push: state changes are pushed asynchronously as server events; they are not solicited one-to-one with a client message (the exception is auction.subscribe, which causes a burst of initial server events).

Acknowledgements

A successful handler returns an ack:

{
  "type": "ack",
  "requestId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "eventType": "auction.bid.place",
  "data": {
    "auctionId": "auc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
    "bidId": "bid_01HQ5BY3K7Z2Z6Y0W5X0W5X0W5X",
    "amount": 25000,
    "stateVersion": 7,
    "idempotencyKey": "bid_req_01HQ5BY3K7Z2Z6Y0W5X0W5X0W5X",
    "wasReplayed": false
  }
}
Field Type Description
type string Always "ack".
requestId string Echos the request's requestId. Omitted if the request had none.
eventType string The event type that produced the acknowledgement.
data object Handler-specific result.

Some handlers return null and deliberately suppress the acknowledgement (for example, internal no-op commands). Do not assume every command yields an ack.

Errors

A failed handler returns an error envelope:

{
  "type": "error",
  "requestId": "req_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "code": "FORBIDDEN",
  "message": "You are not authorized to perform this WebSocket action."
}
Field Type Description
type string Always "error".
requestId string Echoes the failed request's id when the envelope could be parsed.
code string Stable machine-readable error code (see errors.md).
message string Safe, human-readable description. Never contains stack traces.

If a raw frame cannot be parsed as the envelope (e.g. malformed JSON), the server still attempts to extract a requestId; if none is found, requestId is omitted.

The common message envelope

Every client→server message is a JSON object with this envelope:

{
  "type": "auction.bid.place",
  "requestId": "req_01HQ5BWN5Y1Y5RX0W5X0W5X0W5X",
  "timestamp": "2026-08-03T10:00:00.000Z",
  "data": {
    "cycleId": "cyc_01HQ5BWN5Y1Y5RX0W5X0W5X0W5X",
    "amount": "25000",
    "idempotencyKey": "bid_req_01HQ5BY3K7Z2Z6Y0W5X0W5X0W5X"
  }
}
Field Required Type Description
type Yes string Dot-separated event name. 1–128 chars after trimming. Identifies the registered route.
requestId No string Client-generated correlation id, echoed in responses. 1–128 chars. Strongly recommended for debugging and ordering.
timestamp No string Client ISO-8601 timestamp. Informational only; the server does not rely on it for ordering.
data No any Event payload, validated by the per-event Zod schema.

Envelope validation rules:

  • type must be a non-empty trimmed string of at most 128 characters.
  • requestId, when present, must be a trimmed string of 1–128 characters.
  • data is validated against the route's schema; an invalid data yields BAD_REQUEST.
  • Unknown/extra top-level keys are ignored by the base envelope parser.

Server events use the same type / requestId / data shape. For auction domain events, data is structured as:

{
  "type": "bid.created",
  "requestId": "bid_req_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "stateVersion": 7,
    "occurredAt": "2026-08-03T10:00:00.000Z",
    "payload": {
      "id": "bid_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
      "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
      "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
      "subscriberProgramId": "enr_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
      "subscriberId": "sub_01HQ5BWXD1P5RX0W5X0W5X0W5X",
      "bidderName": "Annie Mohan",
      "bidderAvatarUrl": null,
      "bidderCode": "*********3210",
      "bidAmount": "25000.00",
      "bidRank": 1,
      "isWinningBid": true,
      "status": "ACCEPTED",
      "createdAt": "2026-08-03T10:00:00.000Z",
      "serverSequence": 12
    }
  }
}

Client events (Client → Server)

A client event is a command the client sends to the server. It is validated, authorized, rate-limited, and executed. The event pages specify the request schema, response, and required permissions.

Domain Event Permission
System system.ping none
Rooms room.join room:join
Rooms room.leave room:leave
Auction auction.subscribe auction:subscribe
Auction auction.unsubscribe auction:subscribe
Auction auction.bid.place auction:bid:place
Auction auction.bid.cancel (planned) auction:bid:place
Auction auction.status.update auction:staff
Auction auction.pause auction:staff
Auction auction.resume auction:staff
Auction auction.end auction:staff
Auction bid.mark auction:staff
Auction winner.declare auction:staff
Auction winner.record_lot auction:staff
Notification notification.read (planned) notification:read
Chat chat.send (planned) chat:send
Chat chat.typing (planned) chat:typing
Presence presence.update (planned) auction:subscribe

Server events (Server → Client)

A server event is pushed by the server without a one-to-one client request. It is never acknowledged. Clients must handle them idempotently and be able to recover via snapshot + replay.

Domain Event Note
System system.pong Response to system.ping.
Auction auction.connected sent after auction.subscribe.
Auction auction.snapshot full state after subscribe.
Auction presence.snapshot online subscribers after subscribe.
Auction auction.started, auction.paused, auction.resumed, auction.extended, auction.call_started, auction.closed, auction.winner_selected, auction.state_refreshed, auction.status_changed live auction state transitions. auction.closed == documented alias auction.ended.
Auction bid.created, bid.updated, bid.rejected, bid.deleted, prebid.deleted, bidder.disqualified bid lifecycle events. bid.created alias auction.bid.created.
Presence presence.joined, presence.left, presence.changed presence transitions. presence.changed alias presence.update.

Event naming convention

  • Event types use lowercase dot-separated names: domain.action, e.g. auction.bid.place, notification.created, presence.update.
  • The first segment is the domain (system, room, auction, bid, presence, notification, chat, winner).
  • The last segment is the action (join, leave, subscribe, place, update, created, ended).
  • Domain and action are lowerCamelCase nouns/verbs; multi-word actions do not use separators (e.g. record_lot, call_started are legacy underscore compounds retained for compatibility).
  • Server pushes and client commands share the same naming grammar so they compose in one namespace.
  • Client commands tend to use verb actions (place, update); server pushes tend to use past-tense or state actions (created, ended, joined).

Naming rules for implementers

  • Never invent inconsistent casing: auction.bid.place, not auction.bidPlace or AuctionBidPlace.
  • Never reuse a name within a domain for two different shapes.
  • Preserve existing names; additive changes only. See versioning.md.

Message routing

Each message is routed through a fixed middleware pipeline in this order:

  1. Parse the raw text frame as JSON.
  2. Validate the base envelope (type, optional requestId, data).
  3. Resolve the registered route by type.
  4. authenticate — the context must have a user.
  5. rate-limit — per-user and per-IP sliding bucket for the event type.
  6. validate — parse data against the route's Zod schema.
  7. authorize — the connection's permission set must contain all required route permissions.
  8. execute the handler.
  9. send ack or error.

Authorization is message-level: two clients on the same room can observe different routes. See authorization flow and authentication.md.