A web-based tournament management and scoring platform with event-centric workflows, supporting seeding rounds, double-elimination brackets, game queues, and customizable score sheet templates.
- Event Management - Create and manage tournament events with statuses (setup, active, complete, archived)
- Team Management - Register teams, bulk import, check-in workflows, and status tracking
- Seeding Rounds - Multi-round seeding with automatic ranking calculation (top 2 of 3 scores)
- Double Elimination Brackets - Generate brackets (4-64 teams), automatic seeding from rankings, bye handling, and winner advancement
- Game Queue - Ordered queue system for seeding rounds and bracket games with table assignments
- Customizable Score Sheets - Template-driven scoring with text, number, dropdown, button, checkbox, and repeatable group field types
- Score Review System - Admins can accept, reject, or edit submitted scores with full audit trail
- Access Codes - Judges access scoresheets via secure access codes (no login required)
- Google OAuth Authentication - Secure admin login with Google accounts
- Modern UI - Responsive React interface with dark mode support
- Public Views - Public event listings and bracket/seeding displays
Score sheet templates support the following field types:
- Text - Free-form text input
- Number - Numeric input with min/max/step validation
- Dropdown - Select from predefined options
- Buttons - Multiple choice with visual button selection
- Checkbox - Boolean (true/false) values
- Repeatable Group - Repeatable rows of sub-fields (for example, per-stack cube entry)
See Template Schema Guide for detailed schema documentation.
Note: Templates that include
repeatableGroupfields are not portable yet. They cannot be exported with the portable scoresheet tool until portable exporter support for this field type is added.
- Node.js 16+ and npm
- Google Cloud Platform account with OAuth 2.0 credentials (for admin authentication)
cd colosseum
npm install- Go to Google Cloud Console
- Create a new project or select an existing one
- Create OAuth 2.0 credentials:
- Go to "Credentials" -> "Create Credentials" -> "OAuth 2.0 Client ID"
- Application type: Web application
- Authorized redirect URIs:
http://localhost:3000/auth/google/callback
- Copy the Client ID and Client Secret
cp .env.example .envEdit .env and add your Google OAuth credentials:
GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
SESSION_SECRET=your-random-session-secretDevelopment mode (runs both React + Express):
npm run devThis starts two servers:
- Vite dev server (React frontend):
http://localhost:5173- Open this URL in your browser - Express API server (Backend):
http://localhost:3000
The Vite server proxies API calls to Express automatically.
Production mode:
npm run build
npm startIn production, Express serves the built React app at http://localhost:3000.
Important: During development, always use http://localhost:5173 (Vite) for the frontend, NOT port 3000.
A typical tournament follows this workflow:
- Create Event - Admin creates a new event with name, date, location, and number of seeding rounds
- Register Teams - Add teams individually or via bulk import
- Check In Teams - Mark teams as checked in on competition day
- Run Seeding Rounds - Queue seeding rounds, judges score via access codes, admin reviews scores
- Calculate Rankings - System computes rankings from top 2 of 3 seeding scores
- Generate Brackets - Create double-elimination brackets seeded from rankings
- Run Bracket Games - Queue bracket games, judges score, admin reviews, winners advance automatically
- Complete Event - Archive the event when the tournament concludes
- Navigate to the home page
- Click "Enter as Judge"
- Select a score sheet template
- Enter the access code provided by the admin
- Fill out the scoresheet and submit
- Login with Google OAuth
- Create an Event in the Events tab
- Add Teams in the Teams tab (single or bulk import)
- Configure Score Sheets in the Score Sheets tab
- Manage Seeding - View scores, calculate rankings in the Seeding tab
- Create Brackets - Generate brackets from seeding in the Brackets tab
- Manage Queue - Populate and order the game queue in the Queue tab
- Review Scores - Accept, reject, or edit scores in the Scoring tab
colosseum/
├── src/
│ ├── client/ # React frontend
│ │ ├── components/ # Reusable React components
│ │ │ ├── admin/ # Admin panel tabs and modals
│ │ │ ├── bracket/ # Bracket visualization components
│ │ │ └── seeding/ # Seeding table components
│ │ ├── contexts/ # React contexts (Auth, Theme, Event, Chat)
│ │ ├── pages/ # Page components (Home, Judge, Admin, etc.)
│ │ ├── styles/ # Global styles
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Utility functions
│ │ ├── App.tsx # Main React app with routing
│ │ └── main.tsx # React entry point
│ └── server/ # Express backend
│ ├── config/ # OAuth and API configuration
│ ├── database/ # Database connection and schema initialization
│ ├── middleware/ # Authentication middleware
│ ├── routes/ # API route handlers
│ │ ├── auth.ts # Authentication routes
│ │ ├── events.ts # Event CRUD
│ │ ├── teams.ts # Team management
│ │ ├── seeding.ts # Seeding scores and rankings
│ │ ├── brackets.ts # Bracket management
│ │ ├── queue.ts # Game queue
│ │ ├── scores.ts # Score review/approval
│ │ ├── scoresheet.ts # Template management
│ │ ├── audit.ts # Audit log
│ │ └── api.ts # Score submission (judge-facing)
│ ├── services/ # Business logic
│ │ ├── seedingRankings.ts # Seeding calculation
│ │ ├── bracketTemplates.ts # Bracket structure generation
│ │ ├── bracketByeResolver.ts # Bye handling
│ │ └── scoreAccept.ts # Score acceptance logic
│ ├── session/ # Session store
│ └── server.ts # Express server setup
├── docs/ # Documentation
│ ├── TEMPLATE_SCHEMA_GUIDE.md # Score sheet template schema reference
│ └── API_TESTING.md # API testing guide with curl examples
├── templates/ # Example score sheet templates
├── tests/ # Unit & integration tests (Vitest)
├── e2e/ # End-to-end tests (Playwright)
├── database/ # SQLite databases (auto-created)
├── dist/ # Build output
├── playwright.config.ts # Playwright E2E config
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript config
├── package.json
├── Dockerfile # Production Docker image
└── .env # Environment variables (not committed)
The application uses SQLite (development) or PostgreSQL (production) with the following tables:
- users - User accounts and OAuth tokens
- events - Tournament events with status tracking
- teams - Participating teams per event with check-in status
- scoresheet_templates - Score sheet template definitions
- seeding_scores - Individual round scores per team
- seeding_rankings - Computed rankings (seed average, rank, normalized score)
- brackets - Double-elimination bracket containers
- bracket_entries - Teams assigned to brackets with seed positions
- bracket_games - Individual games with advancement routing
- bracket_templates - Pre-defined bracket structures for standard sizes
- score_submissions - Submitted scores with review status
- score_details - Field-by-field score breakdown
- event_scoresheet_templates - Links templates to events for seeding/bracket scoring
- game_queue - Ordered queue of games ready for judging
- audit_log - Change tracking for accountability
GET /auth/google- Initiate Google OAuthGET /auth/google/callback- OAuth callbackGET /auth/user- Get current userGET /auth/logout- Logout
GET /events- List all eventsGET /events/:id- Get event detailsPOST /events- Create eventPATCH /events/:id- Update eventDELETE /events/:id- Delete event
GET /teams/event/:eventId- List teams for eventGET /teams/:id- Get team detailsPOST /teams- Create teamPOST /teams/bulk- Bulk create teamsPATCH /teams/:id- Update teamPATCH /teams/:id/check-in- Check in teamDELETE /teams/:id- Delete team
GET /seeding/scores/event/:eventId- Get all scores for eventGET /seeding/scores/team/:teamId- Get scores for teamPOST /seeding/scores- Submit seeding scorePATCH /seeding/scores/:id- Update scoreDELETE /seeding/scores/:id- Delete scoreGET /seeding/rankings/event/:eventId- Get rankingsPOST /seeding/rankings/recalculate/:eventId- Recalculate rankings
GET /brackets/event/:eventId- List brackets for eventGET /brackets/:id- Get bracket with entries and gamesPOST /brackets- Create bracketPATCH /brackets/:id- Update bracketDELETE /brackets/:id- Delete bracketPOST /brackets/:id/entries- Add entryPOST /brackets/:id/entries/generate- Generate entries from seedingGET /brackets/:id/games- Get gamesPOST /brackets/:id/games- Create gamePATCH /brackets/games/:id- Update gamePOST /brackets/games/:id/advance- Advance winner
GET /queue/event/:eventId- Get queue for eventPOST /queue- Add item to queuePATCH /queue/:id- Update queue itemPATCH /queue/:id/call- Call team/gamePOST /queue/reorder- Reorder queueDELETE /queue/:id- Remove from queue
POST /api/scores/submit- Submit a score (judge-facing)GET /scores/by-event/:eventId- Get scores for event (admin)POST /scores/:id/accept-event- Accept score (event-scoped)POST /scores/:id/revert-event- Revert acceptance
GET /scoresheet/templates- List all templatesGET /scoresheet/templates/:id- Get template detailsPOST /scoresheet/templates- Create templatePUT /scoresheet/templates/:id- Update template
GET /audit/event/:eventId- Get audit log for eventGET /audit/entity/:type/:id- Get audit log for entity
- Frontend: React 19, TypeScript, React Router, Vite
- Backend: Node.js, Express 5, TypeScript
- Database: SQLite (dev) / PostgreSQL (production)
- Authentication: Passport.js with Google OAuth 2.0
- Testing: Vitest (unit/integration), Playwright (E2E)
- Build Tools: Vite (frontend), TypeScript Compiler (backend)
npm run buildBuilds both React frontend and Express backend:
- React app ->
dist/client/ - Express server ->
dist/server/
npm run devRuns both servers concurrently:
- Vite (React with HMR): http://localhost:5173 - Use this for development
- Express (API): http://localhost:3000
Changes to React components update instantly (Hot Module Replacement). Changes to Express server restart automatically (nodemon).
Individual servers:
npm run dev:client # Vite only
npm run dev:server # Express onlyUnit & integration tests (Vitest):
npm test # Run tests in watch mode
npm run test:run # Run tests once
npm run coverage # Run with coverage reportEnd-to-end tests (Playwright):
Playwright tests live in the e2e/ directory and run against the full application (Express API + Vite dev server). The Playwright config (playwright.config.ts) starts both servers automatically via webServer entries, so no manual server setup is needed.
Before your first run, you will need to download Playwright's headless browser builds and their required OS libraries.
This is not handled by npm install.
Note
If you are using the Devcontainer setup, you do not need to run these commands manually.
Simply go straight to npm run test:e2e below.
# First-time setup — download Chromium and install required OS libraries.
# If in an environment without sudo (e.g. container), install in two steps.
# First, run as root to install OS-level libraries and dependencies:
npx playwright install-deps
# Then, as your user, run:
npx playwright install
# If in an environment with sudo access, you can do this in one step with:
npx playwright install --with-depsOnce the dependencies are installed, run Playwright tests with:
# Run E2E tests
npm run test:e2e
npm run lint # ESLint
npm run pretty # Prettier checkPublic and abuse-prone API endpoints are protected by express-rate-limit with per-route policies defined in src/server/middleware/rateLimit.ts.
| Limiter | Endpoints | Window | Limit | Key |
|---|---|---|---|---|
oauthLimiter |
GET /auth/google |
15 min | 20 | IP |
scoreSubmitLimiter |
POST /api/scores/submit |
1 min | 30 | IP |
accessCodeLimiter |
POST /scoresheet/templates/:id/verify |
15 min | 10 | IP + template id |
chatWriteLimiter |
POST /chat/events/:eventId/messages |
1 min | 15 | IP |
chatReadLimiter |
GET /chat/events/:eventId/messages |
1 min | 120 | IP |
queueSyncLimiter |
GET /queue/event/:eventId (sync=1 only) |
1 min | 60 | IP |
publicExpensiveReadLimiter |
GET /events/:id/overall/public, GET /documentation-scores/event/:eventId/public |
1 min | 30 | IP |
- Rate-limit counters use the built-in in-process memory store. This is a deliberate first-pass choice.
- Counters reset on process restart and are not shared across workers or instances.
- This is acceptable for a single-instance deployment or an initial rollout.
Move to a shared store (e.g. Redis via rate-limit-redis) when any of the following apply:
- The app runs on more than one instance (clustering, PM2 workers, Kubernetes replicas)
- Consistent global rate limits are required across restarts
- Stricter abuse controls are needed that survive deploys
- Ensure you're logged in with Google
- Check that OAuth credentials are correctly configured
- Verify redirect URI matches in Google Cloud Console
- Verify the event is selected in the admin panel
- Check that teams have been added to the correct event
- Ensure team status is not "withdrawn" or "no_show"
- Ensure seeding rankings have been calculated first
- Verify the bracket size accommodates the number of teams
- Check that teams are checked in before generating entries
- Rankings must be recalculated manually after score changes
- Use the "Recalculate Rankings" button in the Seeding tab
GNU AGPL v3