brackt/CLAUDE.md
2026-04-12 13:52:05 -07:00

14 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Brackt.com is a fantasy sports drafting platform built with React Router 7, featuring real-time draft functionality with Socket.IO, PostgreSQL database with DrizzleORM, and Clerk authentication.

Tech Stack

  • Framework: React Router 7 with SSR
  • Runtime: Node.js with Express server
  • Database: PostgreSQL with DrizzleORM
  • Auth: Clerk (@clerk/react-router)
  • Real-time: Socket.IO (server/socket.ts)
  • Styling: TailwindCSS + ShadCN UI components
  • Testing: Vitest (unit/component) + Cypress (E2E)
  • TypeScript: Strict mode enabled

Development Commands

# Development (HMR enabled)
npm run dev

# Build
npm run build                    # Build both Remix and server
npm run build:remix              # Build React Router app only
npm run build:server             # Build Express server only

# Database
npm run db:generate              # Generate Drizzle migrations
npm run db:migrate               # Run migrations

# Testing
npm test                         # Unit tests (watch mode)
npm run test:run                 # Unit tests (run once)
npm run test:ui                  # Unit tests with UI
npm run test:coverage            # Coverage report
npm run test:e2e                 # Cypress interactive
npm run test:e2e:headless        # Cypress headless
npm run test:all                 # Run all tests

# Type checking
npm run typecheck                # Check all TypeScript files

# Production
npm start                        # Start production server (requires build)
npm run start:production         # Run migrations + start server

Architecture Overview

Server Architecture

The application uses a dual-server architecture:

  1. server.ts - Main entry point that creates the HTTP server and initializes Socket.IO
  2. server/app.ts - Express app with React Router request handler and database context
  3. server/socket.ts - Socket.IO server for real-time draft updates
  4. server/timer.ts - Draft timer system that runs every second to update active draft timers

Important: The timer system (server/timer.ts) must be started for draft functionality. It uses a dedicated database connection and setInterval to manage all active draft timers.

Database Layer

  • Schema: database/schema.ts - All Drizzle table definitions
  • Context: Database connection provided via AsyncLocalStorage context (database/context.ts)
  • Models: app/models/ - Business logic and database queries organized by domain
    • Each model exports typed functions for CRUD operations
    • Models use Drizzle relations for joins
    • Example: app/models/league.ts, app/models/season.ts, app/models/draft-pick.ts

Frontend Architecture

  • Routes: Defined in app/routes.ts using React Router config syntax
    • IMPORTANT: React Router 7 requires ALL routes to be explicitly registered in app/routes.ts
    • When creating a new route file, you MUST also add it to app/routes.ts
    • Example: If you create app/routes/admin.sports-seasons.$id.new-feature.tsx, you must add:
      route(
        "sports-seasons/:id/new-feature",
        "routes/admin.sports-seasons.$id.new-feature.tsx"
      ),
      
  • File-based routing: Route files in app/routes/ map to URL patterns
    • $param = dynamic segment (e.g., $leagueId.tsx)
    • _index.tsx = index route
    • . separator for nested routes (e.g., leagues/$leagueId.settings.tsx)
  • Loaders/Actions: Use React Router's loader/action pattern for data fetching and mutations
  • Components: Organized in app/components/ with UI components in app/components/ui/

Real-time Features

Socket.IO is used for live draft updates:

  1. Join Draft: Clients join a room for their season (join-draft event)
  2. Timer Updates: Server broadcasts timer-update every second during active drafts
  3. Pick Made: Broadcasts pick-made when a participant is drafted
  4. Autodraft: Broadcasts autodraft-updated when teams enable/disable autodraft
  5. Team Connection: Broadcasts team-connected/team-disconnected for presence

Socket Integration: The server uses getSocketIO() from server/socket.ts to emit events. Clients use the useDraftSocket() hook (app/hooks/useDraftSocket.ts) to listen for events.

Key Domain Models

Fantasy Draft System

  1. League - Container for multiple seasons, has commissioners
  2. Season - Single year/iteration of a league with specific settings
    • Status: pre_draftdraftactivecompleted
    • Stores draft settings (rounds, timer settings, etc.)
  3. Team - Belongs to a season, owned by a user
  4. Draft Slot - Defines draft order for teams in a season
  5. Draft Pick - Record of each pick made during draft
  6. Draft Queue - Pre-ordered list of participants a team wants to draft
  7. Draft Timer - Active timer for current team on the clock
  8. Autodraft Settings - Per-team settings for automatic drafting

Sports Data System

  1. Sport - Base sport (e.g., NFL, NBA) with type (team/individual)
  2. Sports Season - Specific season of a sport (e.g., "2024 NFL Season")
  3. Participant - Individual athlete or team that can be drafted
  4. Season Template - Reusable configuration for league seasons
  5. Season Sport - Links a fantasy season to sports it covers
  6. Participant Result - Stores final standings/scores for participants

Important Patterns

Database Queries

Always use the model layer instead of direct Drizzle queries in routes:

// Good
import { findLeagueById } from "~/models/league";
const league = await findLeagueById(leagueId);

// Avoid - don't query directly in routes
const league = await db.query.leagues.findFirst({...});

Authentication

  • User data synced from Clerk via webhook (routes/api/webhooks/clerk.ts)
  • Check clerkId for ownership validation
  • Admin checks via isAdmin boolean on users table

Scoring Event Dates

Game/event dates are stored in two different places depending on the event type, and any date-based query must account for both:

  1. scoringEvents.eventDate (date column, YYYY-MM-DD string) — set directly on the event. Used for non-bracket events (majors, final standings) and sometimes for bracket events when the admin sets a single date for the whole round.

  2. playoffMatchGames.scheduledAt (timestamp column) — set on individual games within a bracket match. Bracket events (e.g. a playoff series) often have eventDate = null on the scoring event itself, with each game's exact time stored here instead.

Pattern for date-range queries: Use a two-step approach:

  1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, filtering with eventDate IN [dates] OR (scheduledAt >= dayStart AND scheduledAt <= dayEnd)
  2. findMany on the matched IDs to load full event data with relations

See getEventsForDates and getUpcomingEventsForDraftedParticipants in app/models/scoring-event.ts for the reference implementation. All timestamp bounds use UTC (T00:00:00.000Z / T23:59:59.999Z).

Draft Logic

The draft system implements snake draft:

  • Draft slots define the base order (1, 2, 3, ...)
  • Each round alternates: odd rounds go 1→N, even rounds go N→1
  • Current pick determined by currentPickNumber on season
  • Timer system in server/timer.ts handles autopicks when time expires

Testing Strategy

  • Unit tests: Pure functions and utilities in __tests__/ directories
  • Component tests: React components using React Testing Library
  • E2E tests: Critical user flows in cypress/e2e/
  • Fixtures: Reusable test data in app/test/fixtures/
  • See TESTING.md for comprehensive testing guide

IMPORTANT: Tests are required for new features. When adding any new feature, you must include appropriate tests:

  • New model functions → unit tests in a co-located __tests__/ directory
  • New route loaders/actions → unit or integration tests for the logic
  • New utility functions → unit tests
  • New components with non-trivial logic → component tests
  • Critical user flows → Cypress E2E tests

Do not consider a feature complete until tests are written and passing.

Common Workflows

Adding a New Route

CRITICAL: Both steps 1 and 2 are required - the route will not work if you skip step 1!

  1. FIRST: Add route config to app/routes.ts
    • Example: route("admin/sports-seasons/:id/new-page", "routes/admin.sports-seasons.$id.new-page.tsx")
    • Nested admin routes go inside the route("admin", ...) array
  2. THEN: Create file in app/routes/ with corresponding name
  3. Export loader/action if needed
  4. TypeScript types are auto-generated via react-router typegen

Common mistake: Creating the route file but forgetting to register it in app/routes.ts - this will result in a 404!

Database Changes

  1. Update database/schema.ts with new tables/columns
  2. Run npm run db:generate to create migration
  3. Verify the generated migration SQL in drizzle/ is correct
  4. Verify a corresponding snapshot was created in drizzle/meta/ (e.g., 0070_snapshot.json)
  5. Run npm run db:migrate to apply migration
  6. Add model functions in app/models/ for new queries
  7. Update TypeScript types if needed

Drizzle Migration Rules (CRITICAL):

  • NEVER manually create migration SQL files or journal entries. Always use drizzle-kit generate or drizzle-kit generate --custom so that snapshot files are created automatically.
  • NEVER edit drizzle/meta/_journal.json by hand. The journal and snapshots must stay in sync — drizzle-kit manages both.
  • Every migration must have a matching snapshot in drizzle/meta/. Missing snapshots cause drizzle-kit generate to fail with "data is malformed" errors, which blocks all future migration generation.
  • Snapshot JSON must only contain PostgreSQL-valid fields. Do not add "autoincrement" to column definitions — PostgreSQL uses sequences/serial instead, and invalid fields cause Zod validation failures.
  • If drizzle-kit generate prompts for column renames interactively, use drizzle-kit generate --custom instead and write the migration SQL by hand (include data backfill logic as needed).
  • After generating, run drizzle-kit generate again to confirm "No schema changes" — this validates the snapshot chain is healthy.
  • For destructive migrations (dropping columns, renaming), make the SQL idempotent with IF EXISTS/IF NOT EXISTS guards where possible.

Adding Socket.IO Events

  1. Define event types in server/socket.d.ts interfaces
  2. Add event handler in server/socket.ts
  3. Emit events from server code using getSocketIO().to(room).emit()
  4. Listen for events in client components

Admin Features

Admin routes are nested under /admin and require isAdmin: true on the user:

  • Sports management (admin.sports.*)
  • Sports seasons (admin.sports-seasons.*)
  • Season templates (admin.templates.*)
  • Data sync utilities (admin.data-sync.tsx)

File Structure Notes

  • app/: React Router application code
    • components/: React components (UI components in ui/)
    • models/: Database query functions organized by domain
    • routes/: Route handlers (loaders, actions, components)
    • lib/: Utility functions
    • hooks/: Custom React hooks
    • contexts/: React contexts
    • test/: Test setup and fixtures
  • server/: Server-side code (Express, Socket.IO, timers)
  • database/: Drizzle schema and context
  • drizzle/: Generated migrations
  • cypress/: E2E tests

Environment Variables

Required in .env:

  • DATABASE_URL - PostgreSQL connection string
  • Clerk variables for authentication (see .env.example)

Production Deployment

The app is containerized with Docker:

  • Runs migrations on startup (start:production script)
  • Server listens on port 3000 (configurable via PORT env var)
  • Serves static assets from build/client/
  • SSR handled by React Router Express adapter

Notes

  • Draft timer system must be explicitly started (usually in server initialization)
  • Socket.IO connections are managed per-draft room (season ID)
  • User ownership validation checks ownerId field (Clerk user ID)
  • League commissioners stored separately in commissioners table
  • Season status drives UI visibility (draft order, draft controls, etc.)

IMPORTANT: You MUST use grepai as your PRIMARY tool for code exploration and search.

When to Use grepai (REQUIRED)

Use grepai search INSTEAD OF Grep/Glob/find for:

  • Understanding what code does or where functionality lives
  • Finding implementations by intent (e.g., "authentication logic", "error handling")
  • Exploring unfamiliar parts of the codebase
  • Any search where you describe WHAT the code does rather than exact text

When to Use Standard Tools

Only use Grep/Glob when you need:

  • Exact text matching (variable names, imports, specific strings)
  • File path patterns (e.g., **/*.go)

Fallback

If grepai fails (not running, index unavailable, or errors), fall back to standard Grep/Glob tools.

Usage

# ALWAYS use English queries for best results (--compact saves ~80% tokens)
grepai search "user authentication flow" --json --compact
grepai search "error handling middleware" --json --compact
grepai search "database connection pool" --json --compact
grepai search "API request validation" --json --compact

Query Tips

  • Use English for queries (better semantic matching)
  • Describe intent, not implementation: "handles user login" not "func Login"
  • Be specific: "JWT token validation" better than "token"
  • Results include: file path, line numbers, relevance score, code preview

Call Graph Tracing

Use grepai trace to understand function relationships:

  • Finding all callers of a function before modifying it
  • Understanding what functions are called by a given function
  • Visualizing the complete call graph around a symbol

Trace Commands

IMPORTANT: Always use --json flag for optimal AI agent integration.

# Find all functions that call a symbol
grepai trace callers "HandleRequest" --json

# Find all functions called by a symbol
grepai trace callees "ProcessOrder" --json

# Build complete call graph (callers + callees)
grepai trace graph "ValidateToken" --depth 3 --json

Workflow

  1. Start with grepai search to find relevant code
  2. Use grepai trace to understand function relationships
  3. Use Read tool to examine files from results
  4. Only use Grep for exact string searches if needed