Database Design

Entity Relationship Overview

High-level relationships between the core tables.

  ┌──────────┐        ┌──────────────────┐        ┌────────┐
  │  users   │ 1───n  │    entries        │ 1───n  │ picks  │
  │ (id)     │        │ (id, user_id,     │        │(id,    │
  └──────────┘        │  year, bracket)   │        │entry_id│
                      └──────────────────┘        │player_id)
                                                   └────┬───┘
                                                        │ n
                                                        │
  ┌──────────┐                                    ┌─────┴──────┐
  │  teams   │ 1───n  player_pts  n───1  ┌──────┐ │  players   │
  │(espnTeam │        (espnTeam,         │teams │ │(espnPlayer,│
  │  Id,     │         espnPlayer,       └──────┘ │ espnTeam,  │
  │  seed,   │         pts, round,                │ playerName,│
  │  year)   │         result, coach)             │ ppg, year) │
  └──────────┘                                    └────────────┘
        
users
ColumnTypeNotes
idINT PKAuto-increment
full_nameVARCHAR(100)
emailVARCHAR(200)Unique; used for login
cellVARCHAR(20)Unique; formatted (XXX) XXX-XXXX
password_hashVARCHAR(255)Werkzeug PBKDF2 hash
created_atTIMESTAMPAuto
entries
ColumnTypeNotes
idINT PKAuto-increment
user_idINT FK→ users.id
yearINTTournament year
bracket_nameVARCHAR(100)Unique per year
full_nameVARCHAR(100)Copied from user at submit
tiebreakerINTChampionship total guess
tokenCHAR(64)Magic link access
paidTINYINT(1)0 or 1
picks
ColumnTypeNotes
idINT PK
entry_idINT FK→ entries.id (CASCADE DELETE)
espnPlayerIdVARCHAR(20)→ players.espnPlayerId
groupIdINT1–5 (seed group / coaches)
password_reset_tokens
ColumnTypeNotes
idINT PK
user_idINT FK→ users.id (CASCADE DELETE)
tokenCHAR(64)Unique random hex
expires_atDATETIME24 hours from creation
usedTINYINT(1)0 = active, 1 = consumed
Tournament Data Tables

These tables are loaded from ESPN data by the score-fetching pipeline and are not user-editable.

teams

ColumnNotes
espnTeamIdESPN team identifier
teamNameFull team name
seed1–16
gameYearTournament year
play_inFlag: play-in game team
play_in_result'undecided' / 'in' (advanced) / 'out' (eliminated)
predict_from_roundRound scoring begins (play-in adjustment)
outFlag: eliminated from the tournament
predict_finalsFlag: seed projected to reach the Final Four
rankAdmin-set ranking, nullable
espnRankESPN's own ranking, nullable

players

ColumnNotes
espnPlayerIdESPN player ID
espnTeamId→ teams.espnTeamId
playerName"Coach" for coaching picks
ppgSeason points per game
pickDisplay string shown in entry form
gameYearTournament year

player_pts  (one row per player per game)

ColumnNotes
espnPlayerIdPlayer or team ID (for coach rows, equals espnTeamId)
espnTeamIdThe team that played
vsEspnTeamIdThe opponent team
ptsPoints scored in this game (actual or 10 for coach wins)
round1 = Rd 1 … 6 = Championship
result'win', 'loss', 'In-Play'
gameIdESPN game ID
gameYearTournament year
seedTeam seed
coach'Y' = coach/team row, 'N' = player row
Operational Tables

settings  (key-value store)

KeyPurpose
active_yearCurrent tournament year
entries_live'Y'/'N' — lock flag
entries_live_atScheduled go-live datetime
tournament_roundsNumber of rounds (default 6)
payout_example_thresholdPaid entries needed to show real pool
scores_*Score-fetcher config (date, round, frequency)

score_run_log

ColumnNotes
run_idUUID for each fetch run
game_idESPN game ID (if game-specific)
messageLog message text
statusinfo / winner / playing / ok / error
created_atLog timestamp

tournament_config

ColumnNotes
yearUnique per year
stateSETUP / OPEN / LOCKED / IN_PROGRESS / COMPLETE
lock_timeWhen entries locked
current_roundRound the tournament is on
Season Games & Odds Tables

Regular-season data (never NCAA tournament games) and round-of-64 betting lines, loaded by the admin Workflow steps — see How It's Built and Data Warehouse for how this feeds analysis.

games  (one row per team per regular-season game)

ColumnNotes
espnGameIdESPN game ID
espnTeamId / opponentEspnTeamIdThe two teams
gameYear / gameDateSeason year and date
game_typeRegular season / conference tournament
result / siteWin/loss, home/away/neutral
opponent_conference / opponent_conference_tierOpponent's conference, High/Low tier
opponent_final_wins / opponent_final_lossesOpponent's final season record (not as-of-game-date)
opponent_point_diff / opponent_standing_summaryOpponent strength context
team_streak_enteringWin/loss streak entering this game (signed)

player_game_pts  (one row per player per regular-season game)

ColumnNotes
espnPlayerIdPlayer
espnTeamId / espnGameIdTeam and game
gameYear / gameDateSeason year and date
ptsPoints scored in this game
game_typeRegular season / conference tournament

game_odds  (one row per team's round-of-64 odds)

ColumnNotes
espnTeamId / opponentEspnTeamIdThe matchup
gameYear / gameDateTournament year and date
spread / moneyline / totalBetting lines
favorite_espnTeamIdWhich team was favored
result / is_upsetOutcome, whether the underdog won

season_games_load_status  (completion marker, one row per team per year)

ColumnNotes
espnTeamId + gameYearComposite primary key
completed_atOnly written once a team's full season-game loop finishes — lets an interrupted import re-run cleanly instead of skipping partial data
Daily Commentary Tables

Support the admin AI-generated daily update feature (Claude via MCP).

daily_commentary

ColumnNotes
yearTournament year
contentGenerated commentary text
generated_atWhen it was generated
scores_snapshotScore state at generation time

daily_scores_cache

ColumnNotes
yearPrimary key
snapshotScore snapshot, for next-day comparison
saved_atLast saved

commentary_jobs

ColumnNotes
year / statuspending / running / done / error
result / error_messageJob outcome
created_at / updated_atTimestamps