Touchdown is a live NFL/NBA pick game where users choose a 5-player lineup during a live game and score points from real play-by-play events.
The app is built as:
- Next.js client (in client)
- Express + TypeScript API server (in server)
- Firebase (users + picks persistence)
- ESPN APIs (scoreboard, summary, plays)
- Open a game page (
/nfl/game/:gameIdor/nba/game/:gameId). - Pick up to 5 players from the two teams.
- Press Lock In.
- Your lineup is locked for 120 seconds.
- During lock, your players earn points from live plays.
- After cooldown ends, you can swap players and lock again.
- Repeat during the game to maximize score.
- Picks are locked before the game starts.
- Picks unlock 2 minutes before game start.
These rules are based on current implementation in the codebase:
- Roster size: max 5 active picks.
- Swap model: swaps are slot-based (index 1–5). If a slot in NEW is empty, current player in that slot is kept.
- Lock window: every lock starts a fixed 120s cooldown where edits are blocked.
- Multiple lock sessions per game: each lock is saved as a pick submission with timestamp.
- No duplicate player in current/new composition: duplicate adds are prevented in selection flow.
- Auth required to save picks: unauthenticated users see a sign-in gate before lock-in.
A player gets +1 whenever that player appears in athletesInvolved for a play event.
So if athlete A appears in 8 qualifying plays, athlete score is 8 for that scope.
Touchdown tracks 3 score views:
-
Game Score (
gameScores)- All qualifying plays in the full game.
- Per-athlete total, independent of your lock time.
-
Session Score (
sessionScores)- Qualifying plays for your current lineup after your latest lock timestamp.
- Used as “MY SCORE” in current lock session.
-
User Score (
userScores)- Accumulated score per athlete across all your lock periods in the game.
- If you drop and re-add a player later, only plays in active lock periods count.
Total game score for a user is:
If ESPN play-by-play is unavailable for a game (for example old/404 events), the app falls back to stored pick totalScore instead of recomputing from plays.
Each user/game pair is stored in one document:
picks/{gameId}:{displayName}emaildisplayNamegameIdpicks: PickSubmission[]timestamp(last update)teamData/teamLogos(metadata)
Each PickSubmission stores:
players[]timestamp(lock time)totalScore- optional
playerHistoryranges
users/{email}contains profile/auth info and game references (gameIds).
POST /auth/googleGET /auth/google/loginGET /auth/google/callbackGET /auth/test(test login)
POST /api/picks(save lock submission)GET /api/picks/game/:gameId/userGET /api/picks/game/:gameId/user/latestGET /api/picks/game/:gameId/user/historyGET /api/picks/game/:gameId/user/scoresGET /api/picks/game/:gameId/statsGET /api/picks/user/allGET /api/picks/user/all-for-dashboardGET /api/picks/user/all-with-scores
GET /api/leaderboardGET /api/game-data/:league/:gameId
- Node.js 20+
- npm
- Firebase project/service account
- Google OAuth credentials
At repo root:
npm installcd client && npm install
From repo root:
npm run dev
This starts:
- Next.js client on
http://localhost:3000 - Express API on
http://localhost:3001
Create root .env (server reads from server/server.ts bootstrap):
BACKEND_PORT=3001HOST_URL=http://localhost:3001JWT_SECRET=...GOOGLE_OAUTH_CLIENT_ID=...GOOGLE_OAUTH_SECRET=...FIREBASE_PROJECT_ID=...FIREBASE_CLIENT_EMAIL=...FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
Optional build-time client variables (Docker/hosting):
NEXT_PUBLIC_API_URLNEXT_PUBLIC_BASE_URL
- Client computes live scoring from ESPN play logs for dashboard and pick views.
- Server currently returns lightweight score payloads for some endpoints and relies on stored totals for dashboard speed.
- API requests from Next.js are proxied via rewrites in client/next.config.js.
- Docker build is multi-stage and produces a standalone Next.js app + compiled server.
- Procfile runs
npm startfor platform deployment.
GET /api/picks/game/:gameId/user/scorescurrently returns empty score maps and frontend calculates with play logs.- If ESPN play data is unavailable, stored score fallback is used.
- Leaderboard endpoint returns top 10 from
leaderboards/totalScoredocument.
- Client app: client/app
- Client game logic: client/src/views/espn/scoreboard/ChoosePicks.tsx
- Client scoring utility: client/src/utils/scoreCalculation.ts
- Picks API: server/api/picks
- Auth API: server/auth