Files
jackboxpartypack-gamepicker/docs/plans/2026-03-15-vote-tracking-api-design.md
cottongin 4bf41b64cf docs: vote tracking API design (REST + WebSocket)
Covers WebSocket vote.received event, GET /api/sessions/:id/votes
breakdown, GET /api/votes paginated history, and two-phase TDD
testing strategy with regression tests before implementation.

Made-with: Cursor
2026-03-15 18:18:26 -04:00

5.5 KiB

Vote Tracking API Design

Overview

Extend the REST and WebSocket APIs so clients can track votes at both session and global levels. The primary consumer is a stream overlay (ticker-style display) that already has the admin JWT.

Approach

Approach B — Split by resource ownership. Session-scoped vote data lives under the session resource. Global vote history lives under the vote resource. The WebSocket emits real-time events for live votes only.

WebSocket: vote.received Event

Trigger: POST /api/votes/live — fires after the vote transaction succeeds, before the HTTP response. Only live votes emit this event; chat-import does not.

Broadcast target: Session subscribers via broadcastEvent('vote.received', data, sessionId).

Payload:

{
  "type": "vote.received",
  "timestamp": "2026-03-15T20:30:00.000Z",
  "data": {
    "sessionId": 5,
    "game": {
      "id": 42,
      "title": "Quiplash 3",
      "pack_name": "Party Pack 7"
    },
    "vote": {
      "username": "viewer123",
      "type": "up",
      "timestamp": "2026-03-15T20:29:55.000Z"
    },
    "totals": {
      "upvotes": 14,
      "downvotes": 3,
      "popularity_score": 11
    }
  }
}

Implementation notes:

  • votes.js needs access to the WebSocket manager singleton via getWebSocketManager().
  • The existing session games JOIN needs to select pack_name from the games table.

REST: GET /api/sessions/:id/votes

Per-game vote breakdown for a specific session.

Location: backend/routes/sessions.js

Auth: None (matches GET /api/sessions/:id/games).

Response:

{
  "session_id": 5,
  "votes": [
    {
      "game_id": 42,
      "title": "Quiplash 3",
      "pack_name": "Party Pack 7",
      "upvotes": 14,
      "downvotes": 3,
      "net_score": 11,
      "total_votes": 17
    }
  ]
}

Query:

SELECT
  lv.game_id,
  g.title,
  g.pack_name,
  SUM(CASE WHEN lv.vote_type = 1 THEN 1 ELSE 0 END) AS upvotes,
  SUM(CASE WHEN lv.vote_type = -1 THEN 1 ELSE 0 END) AS downvotes,
  SUM(lv.vote_type) AS net_score,
  COUNT(*) AS total_votes
FROM live_votes lv
JOIN games g ON lv.game_id = g.id
WHERE lv.session_id = ?
GROUP BY lv.game_id
ORDER BY net_score DESC

Error handling:

  • Session not found → 404
  • Session exists, no votes → 200 with empty votes array

REST: GET /api/votes

Paginated global vote history with flexible filtering.

Location: backend/routes/votes.js

Auth: None.

Query parameters:

Param Type Default Description
session_id integer Filter by session
game_id integer Filter by game
username string Filter by voter
vote_type up or down Filter by direction
page integer 1 Page number
limit integer 50 Results per page (max 100)

Response:

{
  "votes": [
    {
      "id": 891,
      "session_id": 5,
      "game_id": 42,
      "game_title": "Quiplash 3",
      "pack_name": "Party Pack 7",
      "username": "viewer123",
      "vote_type": "up",
      "timestamp": "2026-03-15T20:29:55.000Z",
      "created_at": "2026-03-15T20:29:56.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 237,
    "total_pages": 5
  }
}

Design notes:

  • vote_type returned as "up" / "down", not raw 1 / -1.
  • game_title and pack_name included via JOIN.
  • Ordered by timestamp DESC.
  • limit capped at 100 server-side.

Error handling:

  • Invalid filter values → 400
  • No results → 200 with empty array and total: 0

Testing Strategy

Phase 1: Regression tests (pre-implementation)

Written and passing before any code changes to lock down existing behavior.

tests/api/regression-votes-live.test.js — existing POST /api/votes/live:

  • Returns 200 with correct response shape (success, session, game, vote)
  • game includes id, title, upvotes, downvotes, popularity_score
  • Increments upvotes/popularity_score for upvote
  • Increments downvotes/decrements popularity_score for downvote
  • 400 for missing fields, invalid vote value, invalid timestamp
  • 404 when no active session or timestamp doesn't match a game
  • 409 for duplicate within 1-second window
  • 401 without JWT

tests/api/regression-games.test.js — game aggregate fields:

  • GET /api/games returns upvotes, downvotes, popularity_score
  • GET /api/games/:id returns same fields
  • Aggregates accurate after votes

tests/api/regression-sessions.test.js — session endpoints:

  • GET /api/sessions/:id returns session object
  • GET /api/sessions/:id returns 404 for nonexistent session
  • GET /api/sessions/:id/games returns game list with expected shape

tests/api/regression-websocket.test.js — existing WebSocket events:

  • Auth flow (auth → auth_success)
  • Subscribe/unsubscribe flow
  • session.started broadcast on session create
  • session.ended broadcast on session close
  • game.added broadcast on game add

Phase 2: New feature tests (TDD — written before implementation)

  • tests/api/votes-get.test.jsGET /api/votes history endpoint
  • tests/api/sessions-votes.test.jsGET /api/sessions/:id/votes breakdown
  • tests/api/votes-live-websocket.test.jsvote.received WebSocket event

Workflow

  1. Write Phase 1 regression tests → run → all green
  2. Write Phase 2 feature tests → run → all red
  3. Implement features
  4. Run all tests → Phase 1 still green, Phase 2 now green