Files
game-scoreboard/docs/DATA_MODEL.md
T
joshuaandClaude Opus 5.5 825eb14f2e Phase 13: questions, sounds and videos on the scoreboard
A game can now carry a list of items the host puts on the scoreboard one at
a time: a question (shown above the standings, with an optional picture and
an answer to reveal), a sound (played through the scoreboard's speakers) or
a video such as a karaoke track (takes over the screen).

- Media is a YouTube link, an https link to a file, or an upload to the
  private game-media bucket. The scoreboard API signs a URL only for the
  item that is on screen.
- What is on screen is state on the event, like the timer, so the ping
  still carries no data. An unrevealed answer and a sound's or video's name
  never reach the scoreboard payload.
- Control room: click an item to show it, or "Random" to have one picked
  (prefers items not played yet). Show/hide answer, pause, take off screen;
  shortcuts Q / A / X.
- Duplicating a game or event copies its items and files; deleting removes
  the files.

Needs supabase/migrations/0006_screen_items.sql applied before deploying.
database.types.ts was edited by hand to match; re-run gen:types after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:54:08 +02:00

3.8 KiB

Data model

Full SQL in supabase/migrations/ (0001_init.sql, then 0002_timer.sql, 0003_buzzer.sql, 0004_buzz_result.sql, 0005_cohosts.sql, 0006_screen_items.sql).

Table Purpose Key columns
events A game night owner_id, scoreboard_token, scoreboard_visibility, frozen_snapshot, current_game_id, reveal_active, reveal_count, timer_visible, timer_duration_ms, timer_remaining_ms, timer_ends_at, screen_item_id, screen_shown_at, screen_answer_shown, screen_paused
teams Teams in an event name, color (#RRGGBB), sort_order, buzzer_token
games Activities scoring_mode, points_mode, multiplier, placement_points int[], status, admin_notes, judge_notes, open_token, open_link_enabled, judging_locked, spotlight_team_id, buzzer_enabled
criteria Scoring blocks per game label, description, input_type, min_value, max_value, step, weight
judges Judges per game name, token, revoked, source (nominated/open_link)
scores One value per (team, criterion, judge) judge_id NULL = admin score (direct mode or override)
adjustments Bonus/penalty points (+/-), reason, optional game_id
buzzes Who buzzed for the current question game_id, team_id, buzzed_at, result (correct/wrong/NULL)
event_cohosts Admins an event is shared with event_id, email (lower case), unique together
game_items Questions, sounds and videos a game can put on the scoreboard game_id, kind (question/sound/video), title, answer, media_url, storage_path, sort_order, played_at

Notes

  • scores unique on (team_id, criterion_id, judge_id) with NULLS NOT DISTINCT, so there's exactly one admin row per team+criterion → clean upserts.
  • In judged games, an admin row (judge_id IS NULL) is an override for that cell.
  • Deleting a criterion/team/judge cascades its scores.
  • Timer (0002_timer.sql): running = timer_ends_at set; paused = timer_remaining_ms set; reset = both null (full timer_duration_ms).
  • Buzzers (0003_buzzer.sql): buzzes is unique on (game_id, team_id) — one buzz per team per question; buzzed_at defaults to clock_timestamp() and decides the order. Clearing the buzzers deletes the game's rows. A trigger checks the team is in the game's event. teams.buzzer_token is a 128-bit token like the others; duplicating an event gives the copied teams new ones.
  • Buzzer answers (0004_buzz_result.sql): buzzes.result is the host's mark. The team answering is the earliest buzz with result IS NULL, unless some buzz is correct (question decided). The point for a correct answer is not stored on the buzz: it is +1 on the admin's scores row for the game's first criterion.
  • Co-hosts (0005_cohosts.sql): a share is keyed by email, not user id, so an admin can be added before they have ever logged in. RLS matches it against the email in the login token. is_event_admin(event) = owner or co-host and guards every table inside an event; deleting the event and editing event_cohosts stay owner-only. A trigger stops events.owner_id from changing.
  • On-screen items (0006_screen_items.sql): title is the question's text, or the host's name for a sound or video. Media is media_url (https link) or storage_path (a file in the private game-media storage bucket, {event id}/{uuid}.{ext}) — never both; a sound or video needs one of them. played_at is set when an item goes on screen and only feeds the random pick. events.screen_item_id is the item on screen (set null if the item is deleted); screen_shown_at changes on every show so screens restart it. The bucket has storage policies for the event's admins only, a 50 MB limit and audio/video/image types.
  • frozen_snapshot holds the ScoreboardPayload captured at freeze time.