Skip to content

Lifecycle

This document describes the complete lifecycle of a WebSocket connection: from opening the socket, authenticating, joining rooms, receiving updates, keeping alive with heartbeats, disconnecting, and reconnecting with state recovery.

Reference: connection.md · authentication.md · rooms.md · events/auction.md


The lifecycle at a glance

Connection
    ↓
Authentication
    ↓
Join Rooms
    ↓
Receive Updates
    ↓
Heartbeat
    ↓
Disconnect
    ↓
Reconnect

Stage 1 — Connection

  1. The client opens a WebSocket to wss://api.example.com/ws (TLS, RFC 6455).
  2. The client supplies an access token (header or bearer.<token> subprotocol) and the websocket.v1 subprotocol.
  3. The server upgrades the request and verifies the token.

Failure at this stage closes the socket with code 1008 / Authentication failed (see connection.md).

Stage 2 — Authentication

  • The server decodes the access token and builds a SocketUser (userId, role, companyId, permissions).
  • There is no unauthenticated window; the socket is only "open" for processing once the token is valid.

See authentication.md.

Stage 3 — Join rooms

After the handshake the client typically:

  • fetches auctionId via REST (e.g. GET /v2/.../auctions/by-cycle/:cycleId — the auction row is guaranteed before READY/LIVE), then subscribes with auction.subscribe (joins auction:<auctionId>),
  • optionally joins company:<id>, user:<id>, notifications, or admins with room.join.

The server authorizes each join; disallowed joins return FORBIDDEN.

Stage 4 — Receive updates

  • auction.subscribe triggers auction.connected, auction.state.snapshot, and auction.audience.snapshot.
  • Live bids, presence transitions, and state changes arrive as server-pushed events (see events/auction.md).
  • The client applies the snapshot as a base and then the deltas.

Stage 5 — Heartbeat

  • The server sends protocol ping every 20 s; clients auto-answer pong.
  • Missed pong → the server terminates the connection (see connection.md).

Stage 6 — Disconnect

On close/error, the server:

  1. stops the heartbeat,
  2. runs every subscription cleanup (unsubscribe from realtime + presence leave),
  3. leaves all rooms,
  4. removes the connection from the connection manager,
  5. logs the closure.

All server-side state tied to the connection is released automatically.

Stage 7 — Reconnect

The client reconnects with a fresh token and restores state:

  1. Open a new socket.
  2. Re-auction.subscribe with lastSequence to replay missed events.
  3. Re-join rooms.
  4. Apply the new auction.state.snapshot.

Async command lifecycle

Write commands (auction.bid.place, auction.pause, auction.winner.declare, …) follow an asynchronous CQRS flow: the gateway acknowledges immediately, then the business result is delivered as a domain event. See protocol.md for the envelope definitions and the frontend migration guide.

sequenceDiagram
    participant C as Client
    participant S as WebSocket Gateway
    participant UC as Command Use Case
    participant E as Event Bus

    C->>S: command { id: cmd_123, type: auction.bid.place, data }
    S->>S: validate + authenticate + authorize
    S->>UC: dispatch (async, fire-and-forget)
    S-->>C: command.acknowledged { correlationId: cmd_123 }
    UC->>UC: validate business rules + transaction
    alt accepted
        UC->>E: publish auction.bid.created
        E-->>S: auction.bid.created
        S-->>C: auction.bid.created { correlationId, sequence }
    else rejected by business rule
        UC-->>S: error
        S-->>C: command.failed { correlationId: cmd_123, errorCode, message }
    end

Sequence diagram

sequenceDiagram
    participant C as Client
    participant S as WebSocket Server
    participant R as Redis Pub/Sub

    Note over C,S: CONNECT
    C->>S: WS upgrade + token (websocket.v1, bearer.<token>)
    alt valid token
        S-->>C: 101 Switching Protocols
    else invalid token
        S--xC: close 1008
    end

    Note over C,S: AUTHENTICATED
    C->>S: auction.subscribe { auctionId, lastSequence? }
    S->>S: authorize + join auction:<auctionId>
    S->>R: ensure ws:room:auction:<auctionId>
    S-->>C: auction.connected
    S-->>C: auction.state.snapshot
    S-->>C: auction.audience.snapshot
    S-->>C: ack

    Note over C,S: UPDATES
    loop live events
        R--)S: ws:room:auction:<auctionId>
        S-->>C: auction.bid.created / auction.state.changed / auction.audience.changed ...
    end

    Note over C,S: HEARTBEAT
    loop every 20s
        S-->>C: ping
        C-->>S: pong
    end

    Note over C,S: DISCONNECT
    alt network failure / close
        C--xS: close
        S->>R: unsubscribe channel when room empty
        S->>S: cleanup rooms + presence + heartbeat
    end

    Note over C,S: RECONNECT
    C->>S: reconnect with fresh token
    C->>S: auction.subscribe { auctionId, lastSequence }
    S-->>C: auction.state.snapshot (+ replayed events)

State transitions

State Entered when Actions Exit
connecting client opens socket — authenticating or closed (handshake fail)
authenticating upgrade request received verify token open or closed (code 1008)
open token valid register connection, start heartbeat closing / closed
subscribed first room/auction join join rooms, subscribe redis, presence join open (on unsubscribe)
closing client/server close run subscription cleanup closed
closed socket closed remove connection, leave rooms, stop heartbeat — (may reconnect)

Resource cleanup checklist (server)

Resource Released on
Heartbeat timer connection close
Room memberships connection close
Redis ws:room:<room> subscription when the room has no members
Auction realtime subscription connection close / auction.unsubscribe
Presence (subscriber) last connection for the enrollment leaves
Connection registry entry connection close

Reconnect checklist (client)

  1. Refresh the access token (REST refresh endpoint).
  2. Open a fresh socket with the new token.
  3. For each active auction: auction.subscribe with lastSequence = last seen serverSequence.
  4. Rejoin company:<id>, user:<id>, notifications, admins as needed.
  5. Discard any in-flight unacknowledged commands that share an id with a previous submission; reuse ids so replays are safe.