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 follows async CQRS (Command Query Responsibility Segregation):

  • Commands (write operations): The client sends a command and receives an immediate command.acknowledged response. Business results are delivered asynchronously via domain events.
  • Queries (read operations): The client sends a query and receives a synchronous response with the requested data.
  • Server push: State changes are pushed asynchronously as domain events. They are not tied one-to-one with client commands.

Command Lifecycle

Client                    Server                    Use Case                Event Bus
  │                         │                          │                        │
  │ 1. auction.bid.place    │                          │                        │
  ├────────────────────────>│                          │                        │
  │                         │ 2. Validate envelope     │                        │
  │                         │    Authenticate          │                        │
  │                         │    Authorize             │                        │
  │                         ├─────────────────────────>│                        │
  │ 3. command.acknowledged │                          │ 3. Execute business    │
  │<────────────────────────┤                          │    logic               │
  │                         │                          │                        │
  │                         │                          │ 4. Publish events      │
  │                         │                          ├───────────────────────>│
  │                         │                          │                        │
  │ 5. auction.bid.created  │<─────────────────────────┼────────────────────────┤
  │<────────────────────────┤                          │                        │

Key Principles:

  • Acknowledgements contain no business data (no bid IDs, amounts, state versions)
  • Acknowledgements normally confirm the command entered the processing pipeline; an idempotent retry may additionally report the command's already-persisted terminal status
  • Initial business outcomes are delivered via domain events; terminal retries report the persisted status in the acknowledgement and require snapshot/feed reconciliation
  • Commands are idempotent (use the envelope id for duplicate detection)

Command Acknowledgements

A command that passes initial validation returns command.acknowledged:

{
  "id": "ack_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "type": "command.acknowledged",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 45218,
  "correlationId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "causationId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "data": {
    "command": "auction.bid.place",
    "status": "accepted"
  },
  "meta": {
    "traceId": "trc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X"
  }
}
Field Type Description
id string Server-generated acknowledgement ID.
type string Always "command.acknowledged".
version string Protocol version. Always "1.0".
source string Emitting service (e.g. dichit-backend).
timestamp string Server ISO-8601 time the ack was emitted.
sequence number Monotonic per-connection sequence (see Sequencing).
correlationId string Echoes the request's id for correlation.
causationId string The command ID that caused this acknowledgement.
data.command string The command type that was accepted.
data.replayedTerminalStatus string Optional completed or failed status for an idempotent retry.
data.errorCode string Present when the replayed terminal status is failed.
data.message string Sanitized public failure message for a failed terminal replay.
data.status string Always "accepted" for acknowledgements.
meta.traceId string Correlation id used in the connection's server logs.

Important: The acknowledgement does not contain business results (bid IDs, amounts, state versions, etc.). Business outcomes arrive via domain events (e.g., auction.bid.created).

Command Rejections

A command that fails validation or authorization returns command.rejected:

{
  "id": "rej_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "type": "command.rejected",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 45219,
  "correlationId": "req_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "causationId": "req_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "data": {
    "command": "auction.bid.place",
    "errorCode": "FORBIDDEN",
    "message": "You are not authorized to place bids in this auction."
  },
  "meta": {
    "traceId": "trc_01HQ5KYUYP1Y5RX0W5X0W5X0W5X"
  }
}

When used: Invalid message format, authentication failure, authorization failure, schema validation failure.

Command Failures

A command that passes validation but fails business rules returns command.failed:

{
  "id": "fail_01HQ5MZUYP1Y5RX0W5X0W5X0W5X",
  "type": "command.failed",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-05T10:00:00.100Z",
  "sequence": 45220,
  "correlationId": "req_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "causationId": "req_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "data": {
    "command": "auction.bid.place",
    "errorCode": "BID_TOO_LOW",
    "message": "Bid amount must be at least $25,100.",
    "details": {
      "minimumBid": 25100,
      "attemptedBid": 25000
    }
  },
  "meta": {
    "traceId": "trc_01HQ5MZUYP1Y5RX0W5X0W5X0W5X"
  }
}

When used: Bid too low, auction closed, participant suspended, insufficient funds, duplicate bid, etc.

Legacy Synchronous Responses (Deprecated)

Query operations (e.g., auction.subscribe, auction.unsubscribe) still return the legacy ack format with business data:

{
  "id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "type": "ack",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 45218,
  "data": {
    "eventType": "auction.subscribe",
    "auctionId": "auc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
    "room": "auction:cyc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X"
  },
  "meta": {
    "traceId": "trc_01HQ5BXWYP1Y5RX0W5X0W5X0W5X"
  }
}

This format is deprecated and will be migrated to the async CQRS pattern in a future release.

Errors

A failed handler returns an error envelope:

{
  "id": "req_01HQ5KYUYP1Y5RX0W5X0W5X0W5X",
  "type": "error",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 45219,
  "data": {
    "code": "FORBIDDEN",
    "message": "You are not authorized to perform this WebSocket action."
  },
  "meta": {
    "traceId": "trc_01HQ5KYUYP1Y5RX0W5X0W5X0W5X"
  }
}
Field Type Description
id string Echoes the failed request's id when the envelope could be parsed.
type string Always "error".
version string Protocol version. Always "1.0".
source string Emitting service (e.g. dichit-backend).
timestamp string Server ISO-8601 time the error was emitted.
sequence number Monotonic per-connection sequence (see Sequencing).
data.code string Stable machine-readable error code (see errors.md).
data.message string Safe, human-readable description. Never contains stack traces.
meta.traceId string Correlation id used in the connection's server logs.

If a raw frame cannot be parsed as the envelope (e.g. malformed JSON), the server still attempts to extract an id; if none is found, the error envelope carries a server-generated id.

The common message envelope

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

{
  "id": "req_01HQ5BWN5Y1Y5RX0W5X0W5X0W5X",
  "type": "auction.bid.place",
  "version": "1.0",
  "source": "web-client",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "data": {
    "auctionId": "auc_01HQ5BWN5Y1Y5RX0W5X0W5X0W5X",
    "amountMinor": "2500000"
  },
  "meta": {
    "traceId": "trc_01HQ5BWN5Y1Y5RX0W5X0W5X0W5X"
  }
}
Field Required Type Description
type Yes string Dot-separated event name. 1–128 chars after trimming. Identifies the registered route.
id No string Client-generated correlation id, echoed in acknowledgements and errors. 1–128 chars. Strongly recommended for debugging and ordering.
version No string Client protocol version. Informational only.
source No string Client identifier for debugging (e.g. web-client, mobile-client). Informational only.
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.
meta No object Reserved extension object.
meta.traceId No string Client trace id, echoed in the server envelope meta for correlation. 1–128 chars.

Envelope validation rules:

  • type must be a non-empty trimmed string of at most 128 characters.
  • id, 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.
  • The server never accepts a sequence from the client; the per-connection sequence is assigned by the server and included only on server→client frames.

The server event envelope

Every server→client message (server push, acknowledgement, or error) uses the same envelope with the addition of the server-assigned sequence:

{
  "id": "evt_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
  "type": "auction.bid.created",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 45220,
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "stateVersion": 7,
    "serverSequence": 12,
    "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-05T10:00:00.000Z"
    }
  },
  "meta": {
    "traceId": "trc_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X"
  }
}
Field Type Description
id string Server-generated unique event id (echoes the client id on acks and errors).
type string Event name. "ack" and "error" are reserved meta types.
version string Protocol version. Always "1.0".
source string Emitting service (e.g. auction-service for auction domain events, dichit-backend for the core envelope).
timestamp string ISO-8601 time the event occurred / was emitted.
sequence number Server-assigned monotonic per-connection sequence.
data object Event payload. Acks carry data.eventType plus the handler result; errors carry data.code and data.message.
meta object Reserved extension object.
meta.traceId string Correlation id for the connection's request log.

For auction domain events, data is structured as { auctionId, cycleId, stateVersion, serverSequence?, payload }. Domain-specific fields such as occurredAt are flattened onto the envelope's top-level timestamp.

Sequencing

The server assigns every outbound frame a per-connection monotonic sequence, tracked in Redis and stamped at delivery time. Each connection has its own counter:

  • Sequences are strictly increasing per connection and never wrap for practical durations.
  • Clients use the envelope sequence to detect dropped frames on a single connection: a gap means messages were lost and the client should recover via snapshot + replay.
  • The envelope sequence is not a global event log position and is not shared across connections or devices.
  • Auction state carries a second, domain-level cursor: data.serverSequence is the per-auction position from the persisted activity log. Clients pass the last observed data.serverSequence as lastSequence when (re)subscribing to fill gaps that happened while disconnected.

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 auction.bid.mark auction:staff
Auction auction.winner.declare auction:staff
Auction 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 — —

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.state.snapshot full state after subscribe.
Auction auction.audience.snapshot online subscribers after subscribe.
Auction auction.state.changed Durable lifecycle transition. data.payload.transition identifies the cause and data.payload.state contains complete post-transition lifecycle state.
Auction auction.bid.created, auction.bid.updated, auction.bid.deleted, auction.prebid.created, auction.prebid.deleted, auction.bidder.disqualified Bid and prebid lifecycle events; auction.prebid.created is durable and staff-only.
Auction auction.winner.changed Durable staff-only winner selection/disqualification detail.
Auction auction.announcement.created, auction.announcement.updated, auction.announcement.deleted auction-scoped announcements.
Auction auction.settings.updated auction schedule/settings changed via program-cycle update.
Audience auction.audience.changed Aggregate audience counters plus the affected participant in both public and staff-scoped copies.

Event naming convention

  • Event types use lowercase dot-separated names: domain.action, e.g. auction.bid.place, notification.created, auction.audience.changed.
  • The first segment is the domain (system, room, auction, notification, chat, winner). Auction sub-concepts use a fixed second segment: auction.bid.*, auction.prebid.*, auction.announcement.*, auction.presence.*, auction.state.*.
  • 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 id, version, source, timestamp, data, meta).
  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.