Skip to main content

WebSocket Protocol

SDD Classification: L3-Technical Authority: Engineering Team Review Cycle: Quarterly
This document defines the WebSocket protocol specification for Relay’s real-time collaboration service, including message formats, handshake flow, connection lifecycle, and error handling.

Protocol Overview


Connection Establishment

WebSocket Endpoint

wss://relay.materi.dev/collab/document/{document_id}

Handshake Request

GET /collab/document/doc_abc123 HTTP/1.1
Host: relay.materi.dev
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: materi-collab-v1
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Handshake Response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: materi-collab-v1

Connection Parameters

ParameterDescriptionExample
document_idTarget document UUIDdoc_abc123
tokenJWT access token (query or header)?token=eyJ...
client_idUnique client identifierAuto-generated UUID
reconnect_idSession ID for reconnectionPrevious session ID

Message Format

Binary Frame Structure

All messages use binary WebSocket frames with Protocol Buffer encoding:
┌────────────────┬────────────────┬─────────────────────┐
│ Message Type   │ Sequence ID    │ Payload (protobuf)  │
│ (1 byte)       │ (4 bytes)      │ (variable)          │
└────────────────┴────────────────┴─────────────────────┘

Message Types

TypeCodeDirectionDescription
OPERATION0x01BidirectionalDocument operation
OPERATION_ACK0x02Server→ClientOperation acknowledgment
PRESENCE0x03Client→ServerPresence update
PRESENCE_BROADCAST0x04Server→ClientPresence broadcast
CURSOR0x05Client→ServerCursor position
CURSOR_BROADCAST0x06Server→ClientCursor broadcast
HEARTBEAT0x07BidirectionalConnection keepalive
SYNC_REQUEST0x08Client→ServerState sync request
SYNC_RESPONSE0x09Server→ClientFull state sync
ERROR0x0FServer→ClientError message

Protocol Buffer Definitions

syntax = "proto3";

message Operation {
  string id = 1;
  string document_id = 2;
  string actor_id = 3;
  uint64 timestamp = 4;
  VectorClock vector_clock = 5;
  oneof op_type {
    InsertOp insert = 6;
    DeleteOp delete = 7;
    FormatOp format = 8;
    RetainOp retain = 9;
  }
}

message InsertOp {
  uint32 position = 1;
  string content = 2;
  map<string, string> attributes = 3;
}

message DeleteOp {
  uint32 position = 1;
  uint32 length = 2;
}

message FormatOp {
  uint32 position = 1;
  uint32 length = 2;
  map<string, string> attributes = 3;
}

message RetainOp {
  uint32 length = 1;
}

message VectorClock {
  map<string, uint64> clocks = 1;
}

message PresenceUpdate {
  string user_id = 1;
  string document_id = 2;
  PresenceState state = 3;
  CursorPosition cursor = 4;
  Selection selection = 5;
  uint64 timestamp = 6;
}

message PresenceState {
  bool online = 1;
  bool typing = 2;
  bool focused = 3;
  string activity = 4;  // "editing", "viewing", "idle"
}

message CursorPosition {
  uint32 line = 1;
  uint32 column = 2;
  uint32 offset = 3;
}

message Selection {
  CursorPosition anchor = 1;
  CursorPosition head = 2;
}

message OperationAck {
  string operation_id = 1;
  VectorClock server_clock = 2;
  bool success = 3;
  string error = 4;
}

message ErrorMessage {
  uint32 code = 1;
  string message = 2;
  string details = 3;
  bool recoverable = 4;
}

Client-to-Server Messages

Operation Message

Sent when user performs an edit:
{
  "type": "operation",
  "id": "op_123456",
  "document_id": "doc_abc123",
  "actor_id": "user_789",
  "vector_clock": {"user_789": 42, "user_456": 38},
  "operation": {
    "insert": {
      "position": 150,
      "content": "Hello, World!",
      "attributes": {"bold": "true"}
    }
  }
}

Presence Update

Sent when user presence changes:
{
  "type": "presence",
  "user_id": "user_789",
  "document_id": "doc_abc123",
  "state": {
    "online": true,
    "typing": true,
    "focused": true,
    "activity": "editing"
  },
  "timestamp": 1704067200000
}

Cursor Update

Sent on cursor movement:
{
  "type": "cursor",
  "user_id": "user_789",
  "document_id": "doc_abc123",
  "cursor": {
    "line": 10,
    "column": 25,
    "offset": 450
  },
  "selection": {
    "anchor": {"line": 10, "column": 20, "offset": 445},
    "head": {"line": 10, "column": 30, "offset": 455}
  }
}

Heartbeat

Sent every 30 seconds:
{
  "type": "heartbeat",
  "timestamp": 1704067200000,
  "client_id": "client_xyz"
}

Sync Request

Request full document state:
{
  "type": "sync_request",
  "document_id": "doc_abc123",
  "last_known_clock": {"user_789": 40, "user_456": 35}
}

Server-to-Client Messages

Operation Broadcast

Broadcast operation from another user:
{
  "type": "operation_broadcast",
  "operation": {
    "id": "op_987654",
    "actor_id": "user_456",
    "vector_clock": {"user_789": 42, "user_456": 39},
    "insert": {
      "position": 200,
      "content": "New text"
    }
  },
  "transformed": true
}

Operation Acknowledgment

Confirm operation receipt:
{
  "type": "operation_ack",
  "operation_id": "op_123456",
  "server_clock": {"user_789": 43, "user_456": 39},
  "success": true
}

Presence Broadcast

Broadcast presence changes:
{
  "type": "presence_broadcast",
  "users": [
    {
      "user_id": "user_456",
      "name": "Alice",
      "avatar_url": "https://...",
      "state": {"online": true, "typing": false, "activity": "viewing"},
      "cursor": {"line": 5, "column": 10, "offset": 200}
    }
  ]
}

Sync Response

Full document state:
{
  "type": "sync_response",
  "document_id": "doc_abc123",
  "content": "Full document content...",
  "vector_clock": {"user_789": 43, "user_456": 39},
  "operations_since": [/* missed operations */],
  "active_users": [/* current collaborators */]
}

Error Message

{
  "type": "error",
  "code": 4003,
  "message": "Permission denied",
  "details": "User does not have write access to this document",
  "recoverable": false
}

Connection Lifecycle

State Machine

Connection States

StateDescriptionClient Action
CONNECTINGOpening WebSocketWait for open event
AUTHENTICATINGValidating JWTWait for auth response
SYNCINGLoading document stateWait for sync_response
ACTIVEReady for operationsSend/receive messages
RECONNECTINGAttempting reconnectExponential backoff
CLOSEDConnection terminatedCleanup resources

Reconnection Strategy

const RECONNECT_CONFIG = {
  initialDelay: 1000,      // 1 second
  maxDelay: 30000,         // 30 seconds
  multiplier: 1.5,
  maxRetries: 10,
  jitter: 0.1              // 10% random jitter
};

function calculateBackoff(attempt) {
  const delay = Math.min(
    RECONNECT_CONFIG.initialDelay * Math.pow(RECONNECT_CONFIG.multiplier, attempt),
    RECONNECT_CONFIG.maxDelay
  );
  const jitter = delay * RECONNECT_CONFIG.jitter * (Math.random() * 2 - 1);
  return delay + jitter;
}

Heartbeat Protocol

Configuration

ParameterValueDescription
Interval30 secondsClient sends heartbeat
Timeout60 secondsServer closes if no response
Grace Period5 secondsBuffer before timeout

Heartbeat Flow

Implementation

// Server-side heartbeat monitoring
pub struct HeartbeatMonitor {
    last_heartbeat: Instant,
    timeout: Duration,
}

impl HeartbeatMonitor {
    pub fn new() -> Self {
        Self {
            last_heartbeat: Instant::now(),
            timeout: Duration::from_secs(60),
        }
    }

    pub fn record_heartbeat(&mut self) {
        self.last_heartbeat = Instant::now();
    }

    pub fn is_expired(&self) -> bool {
        self.last_heartbeat.elapsed() > self.timeout
    }
}

Error Codes

WebSocket Close Codes

CodeNameDescription
4000BAD_REQUESTMalformed message
4001UNAUTHORIZEDInvalid or expired token
4002TOKEN_EXPIREDToken needs refresh
4003FORBIDDENInsufficient permissions
4004NOT_FOUNDDocument not found
4005CONFLICTUnresolvable conflict
4006RATE_LIMITEDToo many messages
4007INTERNAL_ERRORServer error
4008HEARTBEAT_TIMEOUTNo heartbeat received
4009CONNECTION_REPLACEDNew connection from same user
4010SERVER_SHUTDOWNGraceful server shutdown

Error Response Format

{
  "type": "error",
  "code": 4001,
  "message": "Authentication failed",
  "details": "JWT token signature verification failed",
  "recoverable": true,
  "retry_after": 5000
}

Rate Limiting

Limits per Connection

Message TypeLimitWindow
Operations100/secSliding window
Cursor updates50/secThrottled
Presence updates10/secDebounced
Sync requests1/minHard limit

Rate Limit Response

{
  "type": "error",
  "code": 4006,
  "message": "Rate limit exceeded",
  "details": "Operation rate: 150/sec exceeds limit of 100/sec",
  "recoverable": true,
  "retry_after": 1000
}

Compression

Message Compression

Large messages (>1KB) are compressed using LZ4:
┌────────────────┬────────────────┬─────────────────────┐
│ Compression    │ Original Size  │ Compressed Payload  │
│ Flag (1 bit)   │ (4 bytes)      │ (LZ4 compressed)    │
└────────────────┴────────────────┴─────────────────────┘

Compression Thresholds

Payload SizeCompression
< 1KBNone
1KB - 100KBLZ4
> 100KBChunked + LZ4

Security Considerations

Token Validation

  • JWT validated on every connection
  • Token refresh during long sessions
  • Connection closed on permission revocation

Message Validation

  • All operations validated against document schema
  • Input sanitized for XSS prevention
  • Position bounds checked against document length

Rate Limiting

  • Per-user and per-connection limits
  • Graduated throttling before disconnection
  • Abuse detection and blocking

Client Implementation Example

class CollaborationClient {
  private ws: WebSocket;
  private messageQueue: Message[] = [];
  private vectorClock: VectorClock;
  private reconnectAttempts = 0;

  async connect(documentId: string, token: string): Promise<void> {
    const url = `wss://relay.materi.dev/collab/document/${documentId}`;

    this.ws = new WebSocket(url, ['materi-collab-v1']);
    this.ws.binaryType = 'arraybuffer';

    this.ws.onopen = () => {
      // Send authentication
      this.send({
        type: 'auth',
        token: token
      });
    };

    this.ws.onmessage = (event) => {
      const message = this.decode(event.data);
      this.handleMessage(message);
    };

    this.ws.onclose = (event) => {
      if (event.code === 4002) {
        // Token expired, refresh and reconnect
        this.refreshTokenAndReconnect();
      } else if (this.shouldReconnect(event.code)) {
        this.scheduleReconnect();
      }
    };

    this.startHeartbeat();
  }

  sendOperation(operation: Operation): void {
    operation.vectorClock = this.vectorClock.increment(this.userId);
    this.messageQueue.push(operation);
    this.send({
      type: 'operation',
      ...operation
    });
  }

  private startHeartbeat(): void {
    setInterval(() => {
      this.send({
        type: 'heartbeat',
        timestamp: Date.now(),
        client_id: this.clientId
      });
    }, 30000);
  }
}


Document Status: Complete Version: 2.0