# 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:

```json
{
  "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
```json
{
  "type": "ping",
  "timestamp": "2025-12-25T10:30:00Z"
}
```

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

#### Error
```json
{
  "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)
```json
{
  "type": "subscribe",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid"
  }
}
```

#### Subscribed (Server → Client)
```json
{
  "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)
```json
{
  "type": "unsubscribe",
  "timestamp": "2025-12-25T10:30:00Z",
  "data": {
    "track_id": "track-uuid"
  }
}
```

#### Unsubscribed (Server → Client)
```json
{
  "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)
```json
{
  "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)
```json
{
  "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)
```json
{
  "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)
```json
{
  "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)
```json
{
  "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)
```json
{
  "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

```go
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

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

### Parsing Messages

```go
// 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

```typescript
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

```typescript
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

```typescript
// 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)

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

### Standardized Format

```json
{
  "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**
   ```go
   msg := wsmsg.NewWebSocketMessage(msgType, data)
   ```

2. **Include Context Information**
   ```go
   msg.WithTrackID(trackID).
       WithUserID(userID).
       WithRequestID(requestID)
   ```

3. **Use Appropriate Message Types**
   ```go
   wsmsg.MessageTypeSubscribed
   wsmsg.MessageTypeAnalyticsUpdate
   wsmsg.MessageTypeError
   ```

4. **Handle Errors Properly**
   ```go
   errorMsg := wsmsg.NewErrorMessage(400, "Invalid format", nil)
   ```

### For Frontend Developers

1. **Validate Messages**
   ```typescript
   if (!message.type || !message.timestamp) {
     console.error('Invalid message');
     return;
   }
   ```

2. **Handle Errors**
   ```typescript
   if (message.error) {
     console.error('WebSocket error:', message.error);
     // Show user-friendly error
   }
   ```

3. **Use Type Guards**
   ```typescript
   function isAnalyticsUpdate(msg: WebSocketMessage): msg is AnalyticsUpdateMessage {
     return msg.type === 'analytics_update';
   }
   ```

4. **Respect Timestamps**
   ```typescript
   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

```go
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

```typescript
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

