A real-time sports commentary and match tracking system with WebSocket support for live updates. Built with Express.js, PostgreSQL, and WebSockets.
- Real-time Match Updates: Live match creation and status tracking with instant WebSocket notifications
- Commentary System: Post live commentary for matches with detailed event tracking
- Match Management: Create and manage sports matches with automatic status calculation (Scheduled β Live β Finished)
- WebSocket Subscriptions: Subscribe to specific matches and receive real-time commentary updates
- Score Tracking: Track home and away team scores in real-time
- Event Metadata: Store rich event information including actor, team, period, and custom metadata
- Automatic Status Management: Matches automatically transition between scheduled, live, and finished states based on timestamps
- Data Validation: Comprehensive input validation using Zod
- Connection Management: Automatic ping/pong heartbeat to detect and clean up stale connections
- Backend: Express.js 5.2.1
- Database: PostgreSQL with Drizzle ORM
- Real-time: WebSockets (ws 8.19.0)
- Validation: Zod 4.3.6
- Database Migrations: Drizzle Kit
- Environment: Node.js with ES Modules
ScorePulse/
βββ src/
β βββ index.js # Main Express app and server setup
β βββ db/
β β βββ db.js # Database connection and Drizzle instance
β β βββ schema.js # Database schema (Matches and Commentary tables)
β βββ routes/
β β βββ matches.js # Match API endpoints
β β βββ commentary.js # Commentary API endpoints
β βββ validation/
β β βββ matches.js # Zod schemas for matches validation
β β βββ commentary.js # Zod schemas for commentary validation
β βββ utils/
β β βββ match-status.js # Match status calculation logic
β βββ ws/
β βββ server.js # WebSocket server and broadcast logic
βββ drizzle/
β βββ meta/ # Migration metadata
β βββ *.sql # Generated migration files
βββ package.json
βββ drizzle.config.js # Drizzle ORM configuration
βββ README.md
- Node.js 18+
- PostgreSQL 12+
- pnpm (or npm/yarn)
-
Clone the repository
git clone https://github.com/ZammadNasir/score-pulse.git cd scorepulse -
Install dependencies
pnpm install
-
Create
.envfilecp .env.example .env
Add your PostgreSQL connection string:
DATABASE_URL=postgresql://user:password@localhost:5432/scorepulse PORT=8000 HOST=0.0.0.0
-
Set up the database
# Generate initial migration pnpm run db:generate # Run migrations pnpm run db:migrate
-
Start the server
# Development mode (with auto-reload) pnpm run dev # Production mode pnpm start
The server will start on http://localhost:8000 and WebSocket on ws://localhost:8000/ws
http://localhost:8000
GET /matches?limit=50
Query Parameters:
limit(optional): Number of matches to return (default: 50, max: 100)
Response:
{
"data": [
{
"id": 1,
"sport": "football",
"homeTeam": "Manchester United",
"awayTeam": "Liverpool",
"status": "live",
"startTime": "2026-02-28T15:00:00Z",
"endTime": "2026-02-28T17:00:00Z",
"homeScore": 2,
"awayScore": 1,
"createdAt": "2026-02-28T14:55:00Z"
}
]
}POST /matches
Content-Type: application/json
Request Body:
{
"sport": "football",
"homeTeam": "Manchester United",
"awayTeam": "Liverpool",
"startTime": "2026-02-28T15:00:00Z",
"endTime": "2026-02-28T17:00:00Z",
"homeScore": 0,
"awayScore": 0
}Response:
{
"data": {
"id": 1,
"sport": "football",
"homeTeam": "Manchester United",
"awayTeam": "Liverpool",
"status": "scheduled",
"startTime": "2026-02-28T15:00:00Z",
"endTime": "2026-02-28T17:00:00Z",
"homeScore": 0,
"awayScore": 0,
"createdAt": "2026-02-28T14:55:00Z"
}
}GET /matches/:id/commentary?limit=10
Query Parameters:
limit(optional): Number of commentary entries (default: 10, max: 100)
Response:
{
"data": [
{
"id": 1,
"matchId": 1,
"minute": 45,
"sequence": 1,
"period": "first half",
"eventType": "goal",
"actor": "Cristiano Ronaldo",
"team": "Manchester United",
"message": "Goal! Cristiano Ronaldo scores!",
"metadata": { "assist": "Bruno Fernandes" },
"tags": "goal,exciting",
"createdAt": "2026-02-28T15:45:00Z"
}
]
}POST /matches/:id/commentary
Content-Type: application/json
Request Body:
{
"minutes": 45,
"sequence": 1,
"period": "first half",
"eventType": "goal",
"actor": "Cristiano Ronaldo",
"team": "Manchester United",
"message": "Goal! Cristiano Ronaldo scores!",
"metadata": {
"assist": "Bruno Fernandes",
"method": "header"
},
"tags": ["goal", "exciting"]
}Response:
{
"data": {
"id": 1,
"matchId": 1,
"minute": 45,
"sequence": 1,
"period": "first half",
"eventType": "goal",
"actor": "Cristiano Ronaldo",
"team": "Manchester United",
"message": "Goal! Cristiano Ronaldo scores!",
"metadata": { "assist": "Bruno Fernandes" },
"tags": "goal,exciting",
"createdAt": "2026-02-28T15:45:00Z"
}
}Connect to the WebSocket server at ws://localhost:8000/ws
const ws = new WebSocket('ws://localhost:8000/ws');
ws.onopen = () => {
console.log('Connected to WebSocket');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};Send a subscription message with the match ID:
{
"type": "subscribe",
"matchId": 1
}Response:
{
"type": "subscribed",
"matchId": 1
}{
"type": "unsubscribe",
"matchId": 1
}Response:
{
"type": "unsubscribed",
"matchId": 1
}When commentary is posted, all subscribed clients receive:
{
"type": "commentary",
"data": {
"id": 1,
"matchId": 1,
"minute": 45,
"sequence": 1,
"eventType": "goal",
"actor": "Cristiano Ronaldo",
"team": "Manchester United",
"message": "Goal! Cristiano Ronaldo scores!",
"createdAt": "2026-02-28T15:45:00Z"
}
}All connected clients receive:
{
"type": "match_created",
"data": {
"id": 1,
"sport": "football",
"homeTeam": "Manchester United",
"awayTeam": "Liverpool",
"status": "scheduled",
"startTime": "2026-02-28T15:00:00Z",
"endTime": "2026-02-28T17:00:00Z",
"homeScore": 0,
"awayScore": 0,
"createdAt": "2026-02-28T14:55:00Z"
}
}Upon connection:
{
"type": "welcome"
}{
"type": "error",
"message": "Invalid JSON"
}Install wscat globally:
npm install -g wscatTerminal 1 - Connect and Subscribe:
wscat -c ws://localhost:8000/wsOnce connected, subscribe to a match:
{"type": "subscribe", "matchId": 1}
Terminal 2 - Create Commentary:
curl -X POST http://localhost:8000/matches/1/commentary \
-H "Content-Type: application/json" \
-d '{
"minutes": 45,
"eventType": "goal",
"actor": "Player Name",
"team": "Team Name",
"message": "Goal scored!"
}'You should see the commentary broadcast in Terminal 1's wscat connection!
id - Primary key (serial)
sport - Sport type (varchar 50)
homeTeam - Home team name (varchar 100)
awayTeam - Away team name (varchar 100)
status - Match status enum (scheduled, live, finished)
startTime - Match start time (timestamp)
endTime - Match end time (timestamp)
homeScore - Home team score (integer)
awayScore - Away team score (integer)
createdAt - Creation timestamp (timestamp)
id - Primary key (serial)
matchId - Foreign key to matches
minute - Event minute (integer)
sequence - Event sequence (integer)
period - Period/phase name (varchar 20)
eventType - Type of event (varchar 50)
actor - Player/actor name (varchar 100)
team - Team name (varchar 100)
message - Commentary text (text)
metadata - Additional JSON data (jsonb)
tags - Comma-separated tags (text)
createdAt - Creation timestamp (timestamp)
# Generate migrations
pnpm run db:generate
# Run migrations
pnpm run db:migrate
# Open Drizzle Studio (GUI for database)
pnpm run db:studiopnpm run devAuto-reload enabled with --watch flag.
pnpm run dev- Start with auto-reloadpnpm start- Start production serverpnpm run db:generate- Generate database migrationspnpm run db:migrate- Run database migrationspnpm run db:studio- Open Drizzle Studio GUI
Matches transition between states automatically based on timestamps:
Now < startTime β scheduled
startTime β€ Now β€ endTime β live
Now > endTime β finished
All inputs are validated using Zod schemas:
- Match Creation: Sport, team names, timestamps, scores
- Commentary: Minute, event type, actor, team, message
- Time Validation: endTime must be after startTime
- Score Validation: Non-negative integers
- Message Validation: Non-empty strings
The API returns appropriate HTTP status codes:
200- Success201- Resource created400- Invalid request (validation error)500- Server error
ISC
Zammad Nasir