# WebSocket API Documentation **Base URL**: `ws://localhost:8765/ws/sync` **Protocol**: WebSocket (RFC 6455) **Authentication**: Required (JWT token or device token) --- ## Table of Contents 1. [Authentication](#authentication) 2. [Connection](#connection) 3. [Message Format](#message-format) 4. [Message Types](#message-types) 5. [Client Implementation Guide](#client-implementation-guide) 6. [Examples](#examples) 7. [Troubleshooting](#troubleshooting) --- ## Authentication ### Option 1: JWT Token (Web/Mobile Clients) Pass JWT token as query parameter: ``` ws://localhost:8765/ws/sync?token= ``` **How to get JWT token**: ```bash curl -X POST http://localhost:8765/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","password":"password"}' ``` Response: ```json { "token": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "user-uuid", "email": "user@example.com" } } ``` ### Option 2: Device Token (Kobo, KOReader, etc.) Pass device token in Authorization header: ``` ws://localhost:8765/ws/sync?token=any-value Authorization: Bearer ``` **How to get device token**: Device tokens are generated when devices are registered via the API. --- ## Connection ### Step 1: Connect to WebSocket **JavaScript Example**: ```javascript const token = "your-jwt-token"; const ws = new WebSocket(`ws://localhost:8765/ws/sync?token=${token}`); ws.onopen = () => { console.log("Connected to Bookhoard WebSocket"); }; ws.onerror = (error) => { console.error("WebSocket error:", error); }; ws.onclose = (event) => { console.log("WebSocket closed:", event.code, event.reason); }; ``` **Go Example**: ```go import ( "github.com/gorilla/websocket" ) url := "ws://localhost:8765/ws/sync?token=" + token ws, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { log.Fatal("Dial error:", err) } defer ws.Close() ``` **Python Example**: ```python import websocket token = "your-jwt-token" url = f"ws://localhost:8765/ws/sync?token={token}" def on_message(ws, message): print(f"Received: {message}") def on_error(ws, error): print(f"Error: {error}") def on_close(ws, close_status_code, close_msg): print("Closed") def on_open(ws): print("Connected") ws = websocket.WebSocketApp( url, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close ) ws.run_forever() ``` ### Step 2: Handle Initial State Immediately after connecting, you'll receive the initial state: ```javascript ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === "initial_state") { console.log("Initial progress:", message.data.progress); console.log("Connected devices:", message.data.devices); // Store initial state initialState = message.data; } }; ``` ### Step 3: Handle Real-time Updates Listen for subsequent updates: ```javascript ws.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.type) { case "progress_update": handleProgressUpdate(message); break; case "annotation_update": handleAnnotationUpdate(message); break; case "conflict": handleConflict(message); break; case "heartbeat": // Server ping, ignore break; default: console.log("Unknown message type:", message.type); } }; function handleProgressUpdate(message) { const { book_id, percentage } = message.data; const source = message.source_device; console.log(`Progress update for ${book_id}: ${percentage * 100}%`); console.log(`Source: ${source.name} (${source.type})`); // Update UI updateProgressBar(book_id, percentage); } function handleAnnotationUpdate(message) { const { book_id, annotation_type, data } = message.data; const source = message.source_device; console.log(`Annotation update for ${book_id}: ${annotation_type}`); console.log(`Source: ${source.name}`); // Update UI showAnnotation(book_id, data); } function handleConflict(message) { const { book_id, notification_type, conflict_id } = message.data; console.log(`Conflict detected for ${book_id}: ${notification_type}`); // Show conflict resolution UI showConflictDialog(book_id, conflict_id); } ``` --- ## Message Format All messages follow this structure: ```json { "type": "message_type", "timestamp": "2026-02-01T12:00:00Z", "data": { // Message-specific data }, "source_device": { "id": "device-uuid", "name": "Device Name", "type": "kobo|koreader|web|mobile" } } ``` **Fields**: - `type` (string, required): Message type identifier - `timestamp` (string, required): ISO 8601 timestamp - `data` (object, required): Message payload - `source_device` (object, optional): Device that sent the update (not present for initial_state) --- ## Message Types ### 1. initial_state **When**: Immediately after connection **Purpose**: Send current progress for all user's books **Data Structure**: ```json { "type": "initial_state", "timestamp": "2026-02-01T12:00:00Z", "data": { "progress": { "": { "percentage": 0.5, "current_page": 150, "total_pages": 300, "last_read": "2026-02-01T11:30:00Z" } }, "devices": { "kobo": 2, "koreader": 1, "web": 3 } } } ``` **Fields**: - `progress` (object): Map of book UUID → progress data - `percentage` (number): 0.0 to 1.0 - `current_page` (number): Current page number - `total_pages` (number): Total pages in book - `last_read` (string): ISO 8601 timestamp of last read - `devices` (object): Connection statistics by device type ### 2. progress_update **When**: Any device syncs reading progress **Purpose**: Notify all connected clients of progress change **Data Structure**: ```json { "type": "progress_update", "timestamp": "2026-02-01T12:00:00Z", "data": { "book_id": "book-uuid-1", "percentage": 0.75 }, "source_device": { "id": "device-uuid-1", "name": "My Kobo Clara", "type": "kobo" } } ``` **Fields**: - `book_id` (string): UUID of book with updated progress - `percentage` (number): New progress value (0.0 to 1.0) - `source_device` (object): Device that sent the update **Use Cases**: - Update progress bar in real-time - Sync reading position across devices - Update "currently reading" lists ### 3. annotation_update **When**: Any device syncs annotations, bookmarks, or highlights **Purpose**: Share annotations across devices **Data Structure**: ```json { "type": "annotation_update", "timestamp": "2026-02-01T12:00:00Z", "data": { "book_id": "book-uuid-1", "annotation_type": "bookmark", "data": { "page": 150, "text": "Great quote on page 150", "chapter": 5, "created_at": "2026-02-01T12:00:00Z" } }, "source_device": { "id": "device-uuid-1", "name": "My Kobo Clara", "type": "kobo" } } ``` **Fields**: - `book_id` (string): UUID of book - `annotation_type` (string): Type of annotation (bookmark, highlight, note) - `data` (object): Annotation-specific data - `page` (number): Page number - `text` (string): Annotated text or note - `chapter` (number): Chapter number (optional) - `created_at` (string): ISO 8601 timestamp - `source_device` (object): Device that created the annotation **Use Cases**: - Show bookmarks on all devices - Share highlights between devices - Display reading notes ### 4. conflict **When**: Sync conflict is detected **Purpose**: Notify user of conflicting updates **Data Structure**: ```json { "type": "conflict", "timestamp": "2026-02-01T12:00:00Z", "data": { "book_id": "book-uuid-1", "notification_type": "progress_conflict", "conflict_id": "conflict-uuid-1" } } ``` **Fields**: - `book_id` (string): UUID of book with conflict - `notification_type` (string): Type of conflict (progress_conflict, annotation_conflict) - `conflict_id` (string): UUID of conflict record **Use Cases**: - Prompt user to resolve conflict - Show conflict resolution UI - Log conflict for manual review ### 5. heartbeat **When**: Server sends ping every 30 seconds **Purpose**: Keep connection alive **Data Structure**: ```json { "type": "heartbeat", "timestamp": "2026-02-01T12:00:00Z" } ``` **Handling**: Clients should respond automatically (handled by WebSocket protocol) --- ## Client Implementation Guide ### Reconnection Strategy Implement exponential backoff for reconnection: ```javascript let reconnectAttempts = 0; const maxReconnectAttempts = 10; const baseReconnectDelay = 1000; // 1 second function connect() { const token = getAuthToken(); const ws = new WebSocket(`ws://localhost:8765/ws/sync?token=${token}`); ws.onopen = () => { console.log("Connected"); reconnectAttempts = 0; // Reset on successful connection }; ws.onclose = (event) => { if (reconnectAttempts < maxReconnectAttempts) { const delay = baseReconnectDelay * Math.pow(2, reconnectAttempts); console.log(`Reconnecting in ${delay}ms...`); setTimeout(() => { reconnectAttempts++; connect(); }, delay); } else { console.error("Max reconnection attempts reached"); } }; ws.onmessage = handleIncomingMessage; return ws; } ``` ### Error Handling Handle common errors: ```javascript ws.onerror = (error) => { console.error("WebSocket error:", error); // Check specific error types if (error.code === 1006) { console.error("Abnormal closure - server may be down"); } else if (error.code === 1008) { console.error("Policy violation - check authentication"); } }; ``` ### Message Queueing Queue messages while disconnected: ```javascript let messageQueue = []; let isConnected = false; function queueMessage(message) { if (isConnected) { ws.send(JSON.stringify(message)); } else { messageQueue.push(message); } } ws.onopen = () => { isConnected = true; // Send queued messages while (messageQueue.length > 0) { const message = messageQueue.shift(); ws.send(JSON.stringify(message)); } }; ws.onclose = () => { isConnected = false; }; ``` --- ## Examples ### Complete JavaScript Client ```javascript class BookhoardWebSocket { constructor(token) { this.token = token; this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.messageHandlers = {}; } connect() { const url = `ws://localhost:8765/ws/sync?token=${this.token}`; this.ws = new WebSocket(url); this.ws.onopen = () => { console.log("Connected to Bookhoard"); this.reconnectAttempts = 0; }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); }; this.ws.onerror = (error) => { console.error("WebSocket error:", error); }; this.ws.onclose = () => { this.reconnect(); }; } handleMessage(message) { const handler = this.messageHandlers[message.type]; if (handler) { handler(message); } } on(messageType, callback) { this.messageHandlers[messageType] = callback; } reconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { const delay = 1000 * Math.pow(2, this.reconnectAttempts); console.log(`Reconnecting in ${delay}ms...`); setTimeout(() => { this.reconnectAttempts++; this.connect(); }, delay); } } disconnect() { if (this.ws) { this.ws.close(); } } } // Usage const token = "your-jwt-token"; const client = new BookhoardWebSocket(token); client.on("initial_state", (message) => { console.log("Initial state received:", message.data); updateUIWithInitialState(message.data); }); client.on("progress_update", (message) => { console.log("Progress updated:", message.data); updateProgressBar(message.data.book_id, message.data.percentage); }); client.on("annotation_update", (message) => { console.log("Annotation updated:", message.data); showNotification("New annotation from " + message.source_device.name); }); client.on("conflict", (message) => { console.log("Conflict detected:", message.data); showConflictDialog(message.data); }); client.connect(); ``` --- ## Troubleshooting ### Connection Fails **Problem**: Can't connect to WebSocket **Solutions**: 1. Check JWT token is valid 2. Verify server is running: `curl http://localhost:8765/health` 3. Check firewall settings 4. Verify WebSocket URL format ### Connection Drops Frequently **Problem**: WebSocket disconnects unexpectedly **Solutions**: 1. Check network stability 2. Verify server keepalive settings 3. Implement reconnection logic (see above) 4. Check server logs for errors ### No Messages Received **Problem**: Connected but no messages **Solutions**: 1. Check onmessage handler is registered 2. Verify initial_state message received 3. Test with manual progress update via API 4. Check browser console for errors ### Authentication Errors **Problem**: 401 Unauthorized **Solutions**: 1. Verify JWT token is not expired 2. Check token has correct claims (user_id) 3. For device tokens, verify Authorization header format 4. Regenerate token via `/api/auth/login` ### High Memory Usage **Problem**: Client memory increases over time **Solutions**: 1. Clean up old messages 2. Don't store entire message history 3. Use weak references for large data 4. Implement message pagination --- ## Performance Tips 1. **Debounce UI Updates**: Don't update DOM on every message ```javascript const debouncedUpdate = debounce(updateUI, 100); client.on("progress_update", (message) => { debouncedUpdate(message); }); ``` 2. **Use Web Workers** for heavy processing 3. **Limit Message History**: Keep only last N messages 4. **Virtual Scrolling**: For large lists of updates 5. **Connection Pooling**: Reuse connections across tabs --- ## Security Best Practices 1. **Use WSS in Production**: Always use secure WebSocket (wss://) in production 2. **Validate Tokens**: Always verify JWT tokens on server 3. **Rate Limiting**: Implement message rate limiting per connection 4. **Input Sanitization**: Sanitize all message data before displaying 5. **Origin Checks**: Validate WebSocket origin on server --- ## Support For issues or questions: - GitHub Issues: [Bookhoard Repository] - Documentation: [Bookhoard Docs] - API Reference: [Bookhoard API Docs] --- **Last Updated**: 2026-02-01 **Version**: 1.0.0 **Protocol**: WebSocket (RFC 6455)