# 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; }; 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