# Data model Full SQL in `supabase/migrations/0001_init.sql`. | Table | Purpose | Key columns | |---|---|---| | `events` | A game night | `owner_id`, `scoreboard_token`, `scoreboard_visibility`, `frozen_snapshot`, `current_game_id`, `reveal_active`, `reveal_count` | | `teams` | Teams in an event | `name`, `color` (#RRGGBB), `sort_order` | | `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` | | `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` | ## 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. - `frozen_snapshot` holds the `ScoreboardPayload` captured at freeze time.