Live Match Tracker is a production-style full-stack demo for real-time sports data workflows. It models a common sports operations use case: an admin manages a live match feed, while public users watch scores and timeline events update without refreshing the page.
The project is intentionally scoped as a portfolio MVP. It is not presented as production-ready, but it is structured to show practical full-stack engineering decisions: API design, real-time updates, persistence, validation, Docker-based development, CI, and a UI that feels like a real internal tool.
- Full-stack TypeScript across a small monorepo
- Next.js App Router frontend with reusable UI components and responsive layouts
- Express REST API with route validation, centralized error handling, and service-layer business logic
- Socket.IO real-time updates with match-specific rooms
- MongoDB persistence with Mongoose schemas and indexes
- Admin workflows for managing live sports data
- Public-facing pages and an embeddable widget-style view
- Docker Compose local environment with MongoDB, API, and web services
- GitHub Actions CI for install, typecheck, lint, test, build, and Compose validation
- Clear production tradeoffs suitable for technical interview discussion
- Public match list showing teams, score, status, and current minute
- Match detail page with live score, connection indicator, and event timeline
- Admin dashboard for creating matches and operating live match state
- Start, finish, and minute update controls
- Event creation for goals, yellow cards, red cards, substitutions, VAR, and comments
- Automatic score updates when a goal event is assigned to the home or away team
- Inline admin validation and API error feedback
- Compact widget route at
/widget/[matchId] - Socket.IO updates for match creation, state changes, and new events
- Empty states, status badges, and responsive card layouts
flowchart LR
Admin[Admin Dashboard] -->|REST writes| API[Express API]
Public[Public Pages] -->|REST reads| API
Widget[Embeddable Widget] -->|REST reads| API
API --> Mongo[(MongoDB)]
API -->|emit events| Socket[Socket.IO Server]
Public <-->|match room updates| Socket
Widget <-->|match room updates| Socket
Admin <-->|global updates| Socket
The backend owns persistence and business rules. After state changes, it emits Socket.IO events globally or to a match-specific room. Public match detail pages and widgets subscribe to match-specific updates, while the match list can receive global match creation and update events.
apps/
api/ Express API, Socket.IO server, Mongoose models, services, validation
web/ Next.js frontend, Tailwind UI, Socket.IO client, public/admin routes
| Area | Technology |
|---|---|
| Frontend | Next.js, React, TypeScript, Tailwind CSS |
| Backend | Node.js, Express, TypeScript |
| Realtime | Socket.IO |
| Database | MongoDB, Mongoose |
| Validation | Zod |
| Testing | Vitest |
| Tooling | npm workspaces, Docker Compose, GitHub Actions |
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
API health check |
| GET | /api/matches |
List matches |
| POST | /api/matches |
Create a match |
| GET | /api/matches/:id |
Get match details |
| PATCH | /api/matches/:id |
Update match fields |
| GET | /api/matches/:id/events |
List match events |
| POST | /api/matches/:id/events |
Add a live event |
| POST | /api/matches/:id/start |
Start a match |
| POST | /api/matches/:id/finish |
Finish a match |
Validation is handled with Zod at the route boundary. Match lifecycle rules, scoring behavior, and event restrictions live in the service layer.
Clients can join or leave a match room:
| Event | Payload |
|---|---|
match:join |
matchId |
match:leave |
matchId |
The backend emits:
| Event | Payload |
|---|---|
match:created |
{ matchId, match } |
match:updated |
{ matchId, match } |
match:event-added |
{ matchId, match, event } |
match:started |
{ matchId, match } |
match:finished |
{ matchId, match } |
Requirements:
- Node.js 20+
- npm
- MongoDB, unless using Docker Compose
nvm use
npm install
cp .env.example .env
npm run devLocal URLs:
- Web:
http://localhost:3000 - API:
http://localhost:4000 - Health check:
http://localhost:4000/health
For non-Docker local development, set:
MONGODB_URI=mongodb://localhost:27017/live-match-tracker
API_INTERNAL_URL=http://localhost:4000
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_SOCKET_URL=http://localhost:4000Seed demo data:
npm run seedDocker Compose starts the full local stack:
docker compose up --build| Service | Purpose | Local URL |
|---|---|---|
web |
Next.js frontend | http://localhost:3000 |
api |
Express REST API and Socket.IO server | http://localhost:4000 |
mongodb |
MongoDB persistence | mongodb://localhost:27017/live-match-tracker |
Stop the stack:
docker compose downThe web service uses API_INTERNAL_URL=http://api:4000 for server-side requests inside Docker, while browser requests use NEXT_PUBLIC_API_URL=http://localhost:4000.
Seed the Docker database after the stack is running:
docker compose exec api npm run seed| Variable | Description | Example |
|---|---|---|
NODE_ENV |
Runtime environment | development |
MONGODB_URI |
MongoDB connection string | mongodb://localhost:27017/live-match-tracker |
API_PORT |
Backend port | 4000 |
CORS_ORIGIN |
Allowed frontend origin | http://localhost:3000 |
API_INTERNAL_URL |
Server-side API URL used by the web app | http://api:4000 in Docker |
NEXT_PUBLIC_API_URL |
Browser API URL | http://localhost:4000 |
NEXT_PUBLIC_SOCKET_URL |
Browser Socket.IO URL | http://localhost:4000 |
- Start the app with Docker Compose:
docker compose up --build - Seed demo data:
docker compose exec api npm run seed - Open the public match list at
http://localhost:3000 - Open the admin dashboard at
http://localhost:3000/admin - Start a scheduled match or update the minute on the live match
- Add a goal, card, substitution, VAR event, or comment
- Open the public match page or
/widget/[matchId] - Watch the score and timeline update through Socket.IO without refreshing
Capture screenshots from seeded data in a clean browser window. Avoid screenshots from a broken request state, an open developer overlay, or a browser window showing the Next.js development indicator.
For clean screenshots, run the API and MongoDB, seed the database, then capture the web app from a production Next.js build:
docker compose up -d mongodb api
docker compose exec api npm run seed
npm --prefix apps/web run build
npm --prefix apps/web run startThen open http://localhost:3000 and capture the match list, admin dashboard, match detail page, and widget page.
- Admin authentication and protected write routes
- Event editing and deletion for operator corrections
- Match search and status filtering
- League, season, and venue metadata
- Automated match clock support
- E2E coverage for the admin-to-public real-time flow
- Basic observability for API errors and Socket.IO connections
This project is a realistic demo, not a production deployment. Before using this pattern in production, I would add:
- Authentication and authorization for admin routes
- Rate limiting for write endpoints and Socket.IO connections
- Stronger domain validation for event ordering, stoppage time, and match lifecycle transitions
- Structured logging and request correlation
- Monitoring and alerting for API, database, and Socket.IO health
- Error tracking for frontend and backend failures
- Reviewed MongoDB indexes based on real query patterns
- Redis adapter for Socket.IO in multi-instance deployments
- Secrets management instead of plain local
.envfiles - Separate deployment strategy for web, API, database, and persistent storage
- Match-specific Socket.IO rooms keep public match pages and widgets subscribed only to events they need instead of broadcasting every incident globally.
- REST endpoints are responsible for durable state changes and initial reads; WebSocket events notify connected clients after those changes are accepted.
- Scoring rules live in the service layer so route handlers stay focused on HTTP concerns and tests can target domain behavior directly.
- Admin forms show client-side validation first, then surface normalized API errors from Zod validation and service-level lifecycle checks.
- Docker networking requires separate API URLs because server-side web requests use the Compose service name (
api) while browser requests uselocalhost. - Production admin routes would need authentication, authorization, audit logging, CSRF/session strategy or token handling, and stricter operational permissions.
- Socket.IO can scale horizontally with the Redis adapter so room membership and events are shared across API instances.
- Additional tests would add the most value around admin workflows, Socket.IO update delivery, event ordering, and end-to-end score updates.
MIT