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:
cottongin
2026-03-15 16:44:53 -04:00
parent 505c335d20
commit 8ba32e128c
25 changed files with 6546 additions and 0 deletions

399
docs/api/websocket.md Normal file
View 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();
}
```