Live volleyball scoreboard with:
- Required login for every viewer
- Admin-only score controls
- Real-time updates via Socket.IO
- Optional Redis adapter for horizontal scale
- Frontend: React + Vite
- Backend: Node.js + Express + Socket.IO
- Auth: JWT
- Install dependencies:
npm install --workspaces
- Copy env templates:
cp apps/server/.env.example apps/server/.envcp apps/client/.env.example apps/client/.env
- Set Firebase web config in
apps/client/.env:VITE_FIREBASE_API_KEYVITE_FIREBASE_AUTH_DOMAINVITE_FIREBASE_PROJECT_IDVITE_FIREBASE_STORAGE_BUCKETVITE_FIREBASE_MESSAGING_SENDER_IDVITE_FIREBASE_APP_IDVITE_FIREBASE_MEASUREMENT_ID(optional)
- Start backend:
npm run dev:server
- Start frontend:
npm run dev:client
Client runs on http://localhost:5173 and server on http://localhost:4000.
- Create server env file:
cp apps/server/.env.example apps/server/.env
- Set strong values in
apps/server/.env:JWT_SECRETmust be changed.CLIENT_ORIGINshould include your public frontend origin (for examplehttps://mojo-pravah.sa-fet.com).- If you scale sockets across multiple server instances, set
USE_REDIS_ADAPTER=trueand verifyREDIS_URL. - Add Firebase Admin credentials to persist match history:
FIREBASE_PROJECT_IDFIREBASE_CLIENT_EMAILFIREBASE_PRIVATE_KEY(store with escaped newlines like\\n)FIREBASE_DATABASE_URL(optional)
docker compose builddocker compose up -d
Exposed ports:
- Frontend container:
localhost:4173 - Backend container:
localhost:4000
Use this Caddy configuration on your host:
mojo-pravah.sa-fet.com {
reverse_proxy localhost:4173
}
mojo-pravah-backend.sa-fet.com {
reverse_proxy localhost:4000
}The Docker client build is configured to call:
- API:
https://mojo-pravah-backend.sa-fet.com/api - Socket.IO:
https://mojo-pravah-backend.sa-fet.com
docker compose down
By default Redis is not started. To start Redis from compose as well:
docker compose --profile redis up -d
When using Redis, set USE_REDIS_ADAPTER=true in apps/server/.env.
- Login uses Firebase Google sign-in only.
- On first login, a user record is created in Firestore with default role
viewer. - To make someone admin, update their
roletoadminin theuserscollection. - On next login, that user is granted admin access automatically.
- POST /api/auth/google
- GET /api/match/current (auth required)
- GET /api/match/history (auth required)
- PATCH /api/match/score (admin only)
- PATCH /api/match/reset (admin only)
- PATCH /api/match/set-info (admin only, update "Set X Live" banner)
- POST /api/match/history (admin only, saves current complete match)
- PUT /api/match/history/:id (admin only, edit previous match data)
This codebase is already prepared with Socket.IO and efficient event fan-out. For 2000 concurrent users in production, run this setup:
- Deploy at least 2 backend instances behind a load balancer.
- Enable WebSocket sticky sessions at the load balancer.
- Set
USE_REDIS_ADAPTER=trueand provideREDIS_URL. - Put score state in a shared store (Redis or database) if you run multiple instances.
- Add managed monitoring (latency, socket count, memory, CPU).
- Add autoscaling rules and a rolling deployment strategy.
- Move users and scores from memory to PostgreSQL/Redis.
- Rotate JWT secret and reduce token lifetime if needed.
- Add refresh tokens and logout invalidation.
- Add admin audit logs.
- Add integration/load tests (k6 or Artillery) with 2000 socket clients.