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:
- Handshake — the client opens a WebSocket and authenticates with a JWT.
- Ready — the connection is established and can send/receive messages.
- Subscription — the client joins rooms and/or subscribes to auctions.
- Active — the client sends commands and receives pushed updates.
- 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 usesrequestId. - 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:
typemust be a non-empty trimmed string of at most 128 characters.requestId, when present, must be a trimmed string of 1–128 characters.datais validated against the route's schema; an invaliddatayieldsBAD_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
lowerCamelCasenouns/verbs; multi-word actions do not use separators (e.g.record_lot,call_startedare 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, notauction.bidPlaceorAuctionBidPlace. - 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:
- Parse the raw text frame as JSON.
- Validate the base envelope (
type, optionalrequestId,data). - Resolve the registered route by
type. authenticate— the context must have a user.rate-limit— per-user and per-IP sliding bucket for the event type.validate— parsedataagainst the route's Zod schema.authorize— the connection's permission set must contain all required route permissions.- execute the handler.
- send
ackorerror.
Authorization is message-level: two clients on the same room can observe different routes. See authorization flow and authentication.md.
Related documents¶
- README.md — overview and index.
- connection.md — transport and heartbeat details.
- events/auction.md — auction request and server events.
- errors.md — the
errorenvelope codes.