Sportz is a Node.js backend for managing sports matches and live commentary, with real-time delivery over WebSockets.
- Create and list matches
- Create and list commentary for a match
- Automatic match status calculation (
scheduled,live,finished) - Real-time events over WebSocket
- PostgreSQL persistence with Drizzle ORM + migrations
- Request/upgrade protection with Arcjet
- Node.js (ESM)
- Express 5
- WebSocket (
ws) - PostgreSQL
- Drizzle ORM / Drizzle Kit
- Zod validation
- Arcjet security middleware
.
├── src/
│ ├── index.js # App bootstrap + HTTP server
│ ├── arcjet.js # Arcjet middleware/config
│ ├── db/
│ │ ├── db.js # PostgreSQL pool + Drizzle client
│ │ └── schema.js # Drizzle table schemas
│ ├── routes/
│ │ ├── matches.routes.js # /matches REST endpoints
│ │ └── commentary.route.js # /matches/:id/commentary endpoints
│ ├── validation/
│ │ ├── matches.js # Match request/query validation
│ │ └── commentary.js # Commentary request/query validation
│ ├── utils/
│ │ └── match-status.js # Match status computation
│ └── ws/
│ └── server.js # WebSocket server and subscriptions
├── drizzle/ # SQL migrations + metadata
├── drizzle.config.js # Drizzle Kit config
└── package.json
- Node.js 18+ (recommended: latest LTS)
- PostgreSQL database
Create a .env file in the project root:
DATABASE_URL=postgresql://user:password@localhost:5432/sportz
ARCJET_KEY=your_arcjet_key
ARCJET_MODE=DRY_RUN
NODE_ENV=development
PORT=8000
HOST=0.0.0.0DATABASE_URLis required by the app and Drizzle config.ARCJET_KEYis required by current middleware setup.- In development, Arcjet runs in
DRY_RUNmode by default.
npm installGenerate migrations (if schema changes):
npm run db:generateApply migrations:
npm run db:migrateDevelopment mode (with watch):
npm run devProduction mode:
npm startDefault URLs:
- HTTP:
http://localhost:8000 - WebSocket:
ws://localhost:8000/ws
GET /- Response: plain text welcome message
-
GET /matches?limit=50- Query:
limit(optional, max 100)
- Response:
{ "data": [ ...matches ] }
- Query:
-
POST /matches-
Body:
{ "sport": "football", "homeTeam": "Team A", "awayTeam": "Team B", "startTime": "2026-04-27T14:00:00.000Z", "endTime": "2026-04-27T16:00:00.000Z", "homeScore": 0, "awayScore": 0 } -
Creates a match and broadcasts a real-time
match_createdevent.
-
-
GET /matches/:id/commentary?limit=10- Query:
limit(optional, max 100)
- Response:
{ "data": [ ...commentary ] }
- Query:
-
POST /matches/:id/commentary-
Body:
{ "minute": 23, "sequence": 1, "period": "1H", "eventType": "goal", "actor": "Player Name", "team": "Team A", "message": "Great finish into the bottom corner", "metadata": { "xg": 0.42 }, "tags": ["goal", "open-play"] } -
Creates commentary and broadcasts a real-time
commentaryevent for subscribers of the match.
-
Connect to:
ws://<host>:<port>/ws
-
Subscribe to a match:
{ "type": "subscribe", "matchId": 1 } -
Unsubscribe from a match:
{ "type": "unsubscribe", "matchId": 1 }
-
On connect:
{ "type": "welcome" } -
Subscribe acknowledgement:
{ "type": "subscribed", "matchId": 1 } -
Unsubscribe acknowledgement:
{ "type": "unsubscribed", "matchId": 1 } -
Match created broadcast:
{ "type": "match_created", "data": { "...": "match payload" } } -
Commentary broadcast:
{ "type": "commentary", "data": { "...": "commentary payload" } }
npm run dev- start with file watchnpm start- start servernpm run db:generate- generate migrations from schemanpm run db:migrate- run migrations
ISC