docs: comprehensive API documentation from source code
Replace existing docs with fresh documentation built entirely from source code analysis. OpenAPI 3.1 spec as source of truth, plus human-readable Markdown with curl examples, response samples, and workflow guides. - OpenAPI 3.1 spec covering all 42 endpoints (validated against source) - 7 endpoint reference docs (auth, games, sessions, picker, stats, votes, webhooks) - WebSocket protocol documentation (auth, subscriptions, 4 event types) - 4 guide documents (getting started, session lifecycle, voting, webhooks) - API README with overview, auth docs, and quick reference table - Old docs archived to docs/archive/ Made-with: Cursor
This commit is contained in:
399
docs/api/websocket.md
Normal file
399
docs/api/websocket.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# WebSocket Protocol
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The WebSocket API provides real-time updates for Jackbox gaming sessions. Use it to:
|
||||
|
||||
- Receive notifications when sessions start, end, or when games are added
|
||||
- Track player counts as they are updated
|
||||
- Avoid polling REST endpoints for session state changes
|
||||
|
||||
The WebSocket server runs on the same host and port as the HTTP API. Connect to `/api/sessions/live` to establish a live connection.
|
||||
|
||||
---
|
||||
|
||||
## 2. Connection Setup
|
||||
|
||||
**URL:** `ws://host:port/api/sessions/live`
|
||||
|
||||
- Use `ws://` for HTTP and `wss://` for HTTPS
|
||||
- No query parameters are required
|
||||
- Connection can be established without authentication (auth happens via a message after connect)
|
||||
|
||||
**JavaScript example:**
|
||||
|
||||
```javascript
|
||||
const host = 'localhost';
|
||||
const port = 5000;
|
||||
const protocol = 'ws';
|
||||
const ws = new WebSocket(`${protocol}://${host}:${port}/api/sessions/live`);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Connected');
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Authentication
|
||||
|
||||
Authentication is required for subscribing to sessions and for receiving most events. Send your JWT token in an `auth` message after connecting.
|
||||
|
||||
**Send (client → server):**
|
||||
```json
|
||||
{ "type": "auth", "token": "<jwt>" }
|
||||
```
|
||||
|
||||
**Success response:**
|
||||
```json
|
||||
{ "type": "auth_success", "message": "Authenticated successfully" }
|
||||
```
|
||||
|
||||
**Failure responses:**
|
||||
```json
|
||||
{ "type": "auth_error", "message": "Invalid or expired token" }
|
||||
```
|
||||
```json
|
||||
{ "type": "auth_error", "message": "Token required" }
|
||||
```
|
||||
|
||||
**JavaScript example:**
|
||||
|
||||
```javascript
|
||||
// After opening the connection...
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
}));
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'auth_success') {
|
||||
console.log('Authenticated');
|
||||
} else if (msg.type === 'auth_error') {
|
||||
console.error('Auth failed:', msg.message);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Obtain a JWT by calling `POST /api/auth/login` with your admin key.
|
||||
|
||||
---
|
||||
|
||||
## 4. Message Types — Client to Server
|
||||
|
||||
| Type | Required Fields | Description |
|
||||
|-------------|-----------------|--------------------------------------|
|
||||
| `auth` | `token` | Authenticate with a JWT |
|
||||
| `subscribe` | `sessionId` | Subscribe to a session's events |
|
||||
| `unsubscribe`| `sessionId` | Unsubscribe from a session |
|
||||
| `ping` | — | Heartbeat; server responds with `pong` |
|
||||
|
||||
### auth
|
||||
```json
|
||||
{ "type": "auth", "token": "<jwt>" }
|
||||
```
|
||||
|
||||
### subscribe
|
||||
Must be authenticated. You can subscribe to multiple sessions.
|
||||
|
||||
```json
|
||||
{ "type": "subscribe", "sessionId": 3 }
|
||||
```
|
||||
|
||||
### unsubscribe
|
||||
Must be authenticated.
|
||||
|
||||
```json
|
||||
{ "type": "unsubscribe", "sessionId": 3 }
|
||||
```
|
||||
|
||||
### ping
|
||||
```json
|
||||
{ "type": "ping" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Message Types — Server to Client
|
||||
|
||||
| Type | Description |
|
||||
|---------------|------------------------------------------|
|
||||
| `auth_success`| Authentication succeeded |
|
||||
| `auth_error` | Authentication failed |
|
||||
| `subscribed` | Successfully subscribed to a session |
|
||||
| `unsubscribed`| Successfully unsubscribed from a session |
|
||||
| `pong` | Response to client `ping` |
|
||||
| `error` | General error (e.g., not authenticated) |
|
||||
| `session.started` | New session created (broadcast to all authenticated clients) |
|
||||
| `game.added` | Game added to a session (broadcast to subscribers) |
|
||||
| `session.ended` | Session closed (broadcast to subscribers) |
|
||||
| `player-count.updated` | Player count changed (broadcast to subscribers) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Event Reference
|
||||
|
||||
All server-sent events use this envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "<event-type>",
|
||||
"timestamp": "2026-03-15T20:30:00.000Z",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### session.started
|
||||
|
||||
- **Broadcast to:** All authenticated clients (not session-specific)
|
||||
- **Triggered by:** `POST /api/sessions` (creating a new session)
|
||||
|
||||
**Data:**
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"id": 3,
|
||||
"is_active": 1,
|
||||
"created_at": "2026-03-15T20:00:00",
|
||||
"notes": "Friday game night"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### game.added
|
||||
|
||||
- **Broadcast to:** Clients subscribed to the session
|
||||
- **Triggered by:** `POST /api/sessions/{id}/games` (adding a game)
|
||||
|
||||
**Data:**
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"id": 3,
|
||||
"is_active": true,
|
||||
"games_played": 5
|
||||
},
|
||||
"game": {
|
||||
"id": 42,
|
||||
"title": "Quiplash 3",
|
||||
"pack_name": "Jackbox Party Pack 7",
|
||||
"min_players": 3,
|
||||
"max_players": 8,
|
||||
"manually_added": false,
|
||||
"room_code": "ABCD"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### session.ended
|
||||
|
||||
- **Broadcast to:** Clients subscribed to the session
|
||||
- **Triggered by:** `POST /api/sessions/{id}/close` (closing a session)
|
||||
|
||||
**Data:**
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"id": 3,
|
||||
"is_active": 0,
|
||||
"games_played": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### player-count.updated
|
||||
|
||||
- **Broadcast to:** Clients subscribed to the session
|
||||
- **Triggered by:** `PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count`
|
||||
|
||||
**Data:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "3",
|
||||
"gameId": "7",
|
||||
"playerCount": 6,
|
||||
"status": "completed"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Error Handling
|
||||
|
||||
| Type | Message | When |
|
||||
|--------------|----------------------------------------|-----------------------------------------|
|
||||
| `error` | `Not authenticated` | subscribe/unsubscribe without auth |
|
||||
| `error` | `Session ID required` | subscribe without `sessionId` |
|
||||
| `error` | `Unknown message type: foo` | Unknown `type` in client message |
|
||||
| `error` | `Invalid message format` | Unparseable or non-JSON message |
|
||||
| `auth_error` | `Token required` | auth without token |
|
||||
| `auth_error` | `Invalid or expired token` | auth with invalid/expired JWT |
|
||||
|
||||
---
|
||||
|
||||
## 8. Heartbeat and Timeout
|
||||
|
||||
- **Client → Server:** Send `{ "type": "ping" }` periodically
|
||||
- **Server → Client:** Responds with `{ "type": "pong" }`
|
||||
- **Timeout:** If no ping is received for **60 seconds**, the server terminates the connection
|
||||
- **Server check:** The server checks for stale connections every **30 seconds**
|
||||
|
||||
Implement a heartbeat on the client to keep the connection alive:
|
||||
|
||||
```javascript
|
||||
let pingInterval;
|
||||
|
||||
function startHeartbeat() {
|
||||
pingInterval = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}, 30000); // every 30 seconds
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
startHeartbeat();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
clearInterval(pingInterval);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Reconnection
|
||||
|
||||
The server does **not** maintain state across disconnects. After reconnecting:
|
||||
|
||||
1. **Re-authenticate** with an `auth` message
|
||||
2. **Re-subscribe** to any sessions you were tracking
|
||||
|
||||
Implement exponential backoff for reconnection attempts:
|
||||
|
||||
```javascript
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 10;
|
||||
const baseDelay = 1000;
|
||||
|
||||
function connect() {
|
||||
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
ws.send(JSON.stringify({ type: 'auth', token: jwt }));
|
||||
// After auth_success, re-subscribe to sessions...
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
const delay = Math.min(baseDelay * Math.pow(2, reconnectAttempts), 60000);
|
||||
reconnectAttempts++;
|
||||
setTimeout(connect, delay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Complete Example
|
||||
|
||||
Full session lifecycle from connect to disconnect:
|
||||
|
||||
```javascript
|
||||
const JWT = 'your-jwt-token';
|
||||
const WS_URL = 'ws://localhost:5000/api/sessions/live';
|
||||
|
||||
const ws = new WebSocket(WS_URL);
|
||||
let pingInterval;
|
||||
let subscribedSessions = new Set();
|
||||
|
||||
function send(msg) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Connected');
|
||||
send({ type: 'auth', token: JWT });
|
||||
|
||||
pingInterval = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
send({ type: 'ping' });
|
||||
}
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
|
||||
switch (msg.type) {
|
||||
case 'auth_success':
|
||||
console.log('Authenticated');
|
||||
send({ type: 'subscribe', sessionId: 3 });
|
||||
break;
|
||||
|
||||
case 'auth_error':
|
||||
console.error('Auth failed:', msg.message);
|
||||
break;
|
||||
|
||||
case 'subscribed':
|
||||
subscribedSessions.add(msg.sessionId);
|
||||
console.log('Subscribed to session', msg.sessionId);
|
||||
break;
|
||||
|
||||
case 'unsubscribed':
|
||||
subscribedSessions.delete(msg.sessionId);
|
||||
console.log('Unsubscribed from session', msg.sessionId);
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
// Heartbeat acknowledged
|
||||
break;
|
||||
|
||||
case 'session.started':
|
||||
console.log('New session:', msg.data.session);
|
||||
break;
|
||||
|
||||
case 'game.added':
|
||||
console.log('Game added:', msg.data.game.title, 'to session', msg.data.session.id);
|
||||
break;
|
||||
|
||||
case 'session.ended':
|
||||
console.log('Session ended:', msg.data.session.id);
|
||||
subscribedSessions.delete(msg.data.session.id);
|
||||
break;
|
||||
|
||||
case 'player-count.updated':
|
||||
console.log('Player count:', msg.data.playerCount, 'for game', msg.data.gameId);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
case 'auth_error':
|
||||
console.error('Error:', msg.message);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown message:', msg);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => console.error('WebSocket error:', err);
|
||||
ws.onclose = () => {
|
||||
clearInterval(pingInterval);
|
||||
console.log('Disconnected');
|
||||
};
|
||||
|
||||
// Later: unsubscribe and close
|
||||
function disconnect() {
|
||||
subscribedSessions.forEach((sessionId) => {
|
||||
send({ type: 'unsubscribe', sessionId });
|
||||
});
|
||||
ws.close();
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user