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>
3.8 KiB
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
scoresunique on(team_id, criterion_id, judge_id)withNULLS 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_atset; paused =timer_remaining_msset; reset = both null (fulltimer_duration_ms). - Buzzers (
0003_buzzer.sql):buzzesis unique on(game_id, team_id)— one buzz per team per question;buzzed_atdefaults toclock_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_tokenis a 128-bit token like the others; duplicating an event gives the copied teams new ones. - Buzzer answers (
0004_buzz_result.sql):buzzes.resultis the host's mark. The team answering is the earliest buzz withresult IS NULL, unless some buzz iscorrect(question decided). The point for a correct answer is not stored on the buzz: it is +1 on the admin'sscoresrow 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 editingevent_cohostsstay owner-only. A trigger stopsevents.owner_idfrom changing. - On-screen items (
0006_screen_items.sql):titleis the question's text, or the host's name for a sound or video. Media ismedia_url(https link) orstorage_path(a file in the privategame-mediastorage bucket,{event id}/{uuid}.{ext}) — never both; a sound or video needs one of them.played_atis set when an item goes on screen and only feeds the random pick.events.screen_item_idis the item on screen (set null if the item is deleted);screen_shown_atchanges 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_snapshotholds theScoreboardPayloadcaptured at freeze time.