Real-time sports backend built with Express, PostgreSQL (Drizzle ORM), and WebSockets.
The project currently contains a complete backend in Server/ and an empty frontend placeholder in Client/.
- REST API for matches and commentary
- WebSocket pub/sub for match-specific updates
- Input validation with Zod
- Arcjet security middleware for HTTP and WebSocket traffic
- Drizzle ORM + PostgreSQL schema/migrations
- APM Insight agent bootstrap in server startup
Sportz/
Client/ # Frontend placeholder (currently empty)
Server/
src/
app.js # Express + HTTP server bootstrap
ws/server.js # WebSocket server and pub/sub logic
routes/ # API route definitions
controllers/ # Request validation + response shaping
services/ # Business/data-access logic
db/ # Drizzle DB client and schema
middleware/ # Arcjet HTTP/WS middleware
validation/ # Zod schemas
- Node.js 18+
- npm
- PostgreSQL database (Neon or local PostgreSQL)
- Install dependencies:
cd Server
npm install- Configure environment variables:
Create/update Server/.env:
PORT=3000
HOST=0.0.0.0
NODE_ENV=development
DATABASE_URL=postgresql://<DB_USER>:<DB_PASSWORD>@<HOST>:<PORT>/<DB_NAME>?sslmode=verify-full&channel_binding=require
ARCJET_KEY=<your_arcjet_key>
ARCJET_ENV=development
BASE_URL=https://sportz-sigma.vercel.appNotes:
ARCJET_ENV=developmentruns Arcjet in DRY_RUN mode in this project.- WebSocket bot rule is enforced only in
LIVEmode. NODE_ENV=developmentmakes the app use localhost URLs for startup logs.- In non-development environments, the app uses
BASE_URL(for example, your deployed URL).
- Run migrations (if needed):
npm run db:migrate- Start the development server:
npm run devExpected startup logs:
Server running on http://localhost:3000WebSocket server running on ws://0.0.0.0:3000/ws
Base URL: http://localhost:3000/api/v1
GET /
GET /matches?limit=50POST /matches
Example POST /matches body:
{
"sport": "Football",
"homeTeam": "Team A",
"awayTeam": "Team B",
"startTime": "2026-04-02T12:00:00.000Z",
"endTime": "2026-04-02T14:00:00.000Z",
"homeScore": 0,
"awayScore": 0
}GET /matches/:id/commentary?limit=100POST /matches/:id/commentary
Example POST /matches/1/commentary body:
{
"minute": 42,
"sequence": 1,
"period": "H2",
"eventType": "goal",
"actor": "Alex Morgan",
"team": "FC Neon",
"message": "GOAL! Powerful finish from the edge of the box.",
"metadata": { "assist": "Sam Kerr" },
"tags": ["goal", "shot"]
}Important: use minute (not minutes).
Endpoint:
ws://localhost:3000/ws
Connect using:
wscat -c ws://localhost:3000/ws{"type":"subscribe","matchId":1}{"type":"unsubscribe","matchId":1}WebSocket_server_created(on connect)client_joined(broadcast when a client connects)subscribed_to_matchunsubscribed_from_matchmatch_created_at(for subscribers of that match)commentary_update(for subscribers of that match)
Most responses use:
{
"payload": {
"success": true,
"error": null,
"data": {}
}
}Validation failures return success: false and issue details in data.
If error details mention DNS/host resolution like EAI_AGAIN, your database host is unreachable from your machine/network.
Checklist:
- Verify
DATABASE_URLhost/user/password - Confirm internet/DNS access to your DB host
- Try a local PostgreSQL URL temporarily to isolate network issues
- Keep
ARCJET_ENV=developmentfor local runs - In this project, WebSocket detectBot is only active in
LIVE
- Ensure you sent a valid subscribe message
- Ensure
matchIdis an integer - Ensure your subscription
matchIdmatches the eventmatch.id
npm run dev- Start dev server with nodemonnpm start- Start servernpm run db:generate- Generate Drizzle migrationsnpm run db:migrate- Run migrations
- Build frontend in
Client/to consume REST + WebSocket streams - Add automated tests for controllers/services/ws flows
- Add structured logging for DB and WebSocket diagnostics