veza/WEBSOCKET_MESSAGE_FORMAT.md

11 KiB

WebSocket Message Format Standardization

INT-014: Add WebSocket message format standardization

Date: 2025-12-25
Status: Completed

Overview

This document defines the standardized format for all WebSocket messages in the Veza platform. It ensures consistency between backend and frontend, making message handling predictable and maintainable.

Standard Message Format

All WebSocket messages follow this standardized structure:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "message_type",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {},
  "error": null,
  "request_id": "req-123",
  "user_id": "user-uuid",
  "track_id": "track-uuid",
  "conversation_id": "conv-uuid"
}

Required Fields

  • type (string, required): Message type identifier
  • timestamp (string, required): ISO 8601 timestamp (RFC3339) in UTC

Optional Fields

  • id (string, optional): Unique message ID (UUID)
  • data (object, optional): Message payload
  • error (object, optional): Error information (for error messages)
  • request_id (string, optional): Request ID for correlation
  • user_id (string, optional): User ID (UUID)
  • track_id (string, optional): Track ID (UUID or string)
  • conversation_id (string, optional): Conversation ID (UUID)

Message Types

Connection Messages

Ping

{
  "type": "ping",
  "timestamp": "2025-12-25T10:30:00Z"
}

Pong

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "pong",
  "timestamp": "2025-12-25T10:30:00Z"
}

Error

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "error",
  "timestamp": "2025-12-25T10:30:00Z",
  "error": {
    "code": 400,
    "message": "Invalid message format",
    "details": {
      "field": "type",
      "reason": "Missing required field"
    }
  }
}

Subscription Messages

Subscribe (Client → Server)

{
  "type": "subscribe",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid"
  }
}

Subscribed (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "subscribed",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid"
  },
  "track_id": "track-uuid"
}

Unsubscribe (Client → Server)

{
  "type": "unsubscribe",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid"
  }
}

Unsubscribed (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "unsubscribed",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid"
  },
  "track_id": "track-uuid"
}

Chat Messages

Chat Message (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "chat_message",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "message": {
      "id": "msg-uuid",
      "conversation_id": "conv-uuid",
      "sender_id": "user-uuid",
      "content": "Hello, world!",
      "created_at": "2025-12-25T10:30:00Z"
    }
  },
  "conversation_id": "conv-uuid"
}

Typing Indicator (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "typing",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "user_id": "user-uuid",
    "conversation_id": "conv-uuid",
    "is_typing": true
  },
  "conversation_id": "conv-uuid"
}

Playback Messages

Analytics Update (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "analytics_update",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "id": "analytics-uuid",
    "track_id": "track-uuid",
    "user_id": "user-uuid",
    "play_time": 120,
    "pause_count": 2,
    "seek_count": 1,
    "completion_rate": 0.75
  },
  "track_id": "track-uuid"
}

Stats Update (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "stats_update",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "total_sessions": 100,
    "total_play_time": 3600,
    "average_play_time": 36,
    "completion_rate": 0.65
  },
  "track_id": "track-uuid"
}

Playback State (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "playback_state",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid",
    "user_id": "user-uuid",
    "position": 45.5,
    "is_playing": true,
    "volume": 0.8
  },
  "track_id": "track-uuid"
}

Notification Messages

Notification (Server → Client)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "notification",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "id": "notif-uuid",
    "user_id": "user-uuid",
    "type": "new_message",
    "content": "You have a new message",
    "link": "/conversations/conv-uuid",
    "read": false,
    "created_at": "2025-12-25T10:30:00Z"
  },
  "user_id": "user-uuid"
}

Backend Implementation

Creating Messages

import wsmsg "veza-backend-api/internal/websocket"

// Create a simple message
msg := wsmsg.NewWebSocketMessage(
    wsmsg.MessageTypeSubscribed,
    gin.H{"track_id": trackID},
)

// Add context information
msg.WithTrackID(trackID).
    WithUserID(userID.String()).
    WithRequestID(requestID)

// Convert to JSON
data, err := msg.ToJSON()

Error Messages

// Create an error message
errorMsg := wsmsg.NewErrorMessage(
    400,
    "Invalid message format",
    map[string]interface{}{
        "field": "type",
        "reason": "Missing required field",
    },
)

Parsing Messages

// Parse incoming message
msg, err := wsmsg.ParseWebSocketMessage(messageBytes)
if err != nil {
    // Handle error
}

// Validate message
if !msg.IsValid() {
    // Handle invalid message
}

// Access message fields
switch msg.Type {
case "subscribe":
    // Handle subscription
case "ping":
    // Handle ping
}

Frontend Implementation

TypeScript Types

interface WebSocketMessage {
  id?: string;
  type: string;
  timestamp: string;
  data?: unknown;
  error?: {
    code: number;
    message: string;
    details?: Record<string, unknown>;
  };
  request_id?: string;
  user_id?: string;
  track_id?: string;
  conversation_id?: string;
}

Handling Messages

ws.onmessage = (event) => {
  try {
    const message: WebSocketMessage = JSON.parse(event.data);
    
    // Validate message
    if (!message.type || !message.timestamp) {
      console.error('Invalid WebSocket message format');
      return;
    }
    
    // Handle by type
    switch (message.type) {
      case 'subscribed':
        handleSubscribed(message);
        break;
      case 'analytics_update':
        handleAnalyticsUpdate(message);
        break;
      case 'error':
        handleError(message);
        break;
      default:
        console.warn('Unknown message type:', message.type);
    }
  } catch (error) {
    console.error('Failed to parse WebSocket message:', error);
  }
};

Sending Messages

// Send a subscribe message
const subscribeMessage: WebSocketMessage = {
  type: 'subscribe',
  timestamp: new Date().toISOString(),
  data: {
    track_id: trackId,
  },
};

ws.send(JSON.stringify(subscribeMessage));

Migration from Legacy Format

Legacy Format (Deprecated)

{
  "track_id": 123,
  "type": "analytics_update",
  "data": {},
  "timestamp": "2025-12-25T10:30:00Z"
}

Standardized Format

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "analytics_update",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {},
  "track_id": "123"
}

Key Changes

  1. Message ID: Added unique id field (UUID)
  2. Track ID: Changed from track_id (int64) to track_id (string)
  3. Timestamp: Always ISO 8601 (RFC3339) format
  4. Error Handling: Standardized error object structure
  5. Context Fields: Added request_id, user_id, conversation_id

Best Practices

For Backend Developers

  1. Always Use Standardized Format

    msg := wsmsg.NewWebSocketMessage(msgType, data)
    
  2. Include Context Information

    msg.WithTrackID(trackID).
        WithUserID(userID).
        WithRequestID(requestID)
    
  3. Use Appropriate Message Types

    wsmsg.MessageTypeSubscribed
    wsmsg.MessageTypeAnalyticsUpdate
    wsmsg.MessageTypeError
    
  4. Handle Errors Properly

    errorMsg := wsmsg.NewErrorMessage(400, "Invalid format", nil)
    

For Frontend Developers

  1. Validate Messages

    if (!message.type || !message.timestamp) {
      console.error('Invalid message');
      return;
    }
    
  2. Handle Errors

    if (message.error) {
      console.error('WebSocket error:', message.error);
      // Show user-friendly error
    }
    
  3. Use Type Guards

    function isAnalyticsUpdate(msg: WebSocketMessage): msg is AnalyticsUpdateMessage {
      return msg.type === 'analytics_update';
    }
    
  4. Respect Timestamps

    const messageTime = new Date(message.timestamp);
    const now = new Date();
    const delay = now.getTime() - messageTime.getTime();
    

Message Type Reference

Type Direction Description
ping Client → Server Keep-alive ping
pong Server → Client Keep-alive pong
error Server → Client Error message
subscribe Client → Server Subscribe to updates
unsubscribe Client → Server Unsubscribe from updates
subscribed Server → Client Subscription confirmed
unsubscribed Server → Client Unsubscription confirmed
chat_message Server → Client New chat message
typing Bidirectional Typing indicator
read_receipt Bidirectional Read receipt
analytics_update Server → Client Playback analytics update
stats_update Server → Client Playback stats update
playback_state Bidirectional Playback state sync
notification Server → Client Notification event

Error Codes

Code Description
400 Bad Request - Invalid message format
401 Unauthorized - Authentication required
403 Forbidden - Insufficient permissions
404 Not Found - Resource not found
429 Too Many Requests - Rate limit exceeded
500 Internal Server Error - Server error

Testing

Backend Tests

func TestWebSocketMessage(t *testing.T) {
    msg := wsmsg.NewWebSocketMessage(
        wsmsg.MessageTypeSubscribed,
        gin.H{"track_id": "123"},
    )
    
    assert.NotEmpty(t, msg.ID)
    assert.Equal(t, "subscribed", msg.Type)
    assert.NotEmpty(t, msg.Timestamp)
    assert.True(t, msg.IsValid())
}

Frontend Tests

describe('WebSocket Message', () => {
  it('should parse valid message', () => {
    const message = {
      id: '123',
      type: 'subscribed',
      timestamp: '2025-12-25T10:30:00Z',
      data: { track_id: 'track-123' },
    };
    
    expect(message.type).toBe('subscribed');
    expect(message.timestamp).toBeDefined();
  });
});

References

  • DATETIME_STANDARD.md - Date/time format specification
  • ERROR_RESPONSE_STANDARD.md - Error format specification
  • veza-backend-api/internal/websocket/message.go - Backend implementation
  • apps/web/src/types/websocket.ts - Frontend types

Last Updated: 2025-12-25
Maintained By: Veza Backend Team