Server runs on http://localhost:3000
- Custom HTTP + Socket.io server
- Redis adapter (pub/sub for scaling)
- JWT authentication middleware
- Ping/pong heartbeat
- Room state management (status, mode, timerEndTime, activeProblemId, maxCapacity)
- User presence tracking (Redis Sets)
- Room capacity enforcement
- Media state persistence
- Typing users with 3-second TTL
- Socket-to-user mapping
Presence:
join_room- Capacity check, emits room_error if fullleave_room- Cleanup and broadcastdisconnect- Full cleanuptoggle_media- Media state with Redis persistence
Timer:
timer_start- Server-side completion with setTimeouttimer_pause- Calculate remaining timetimer_reset- Clear statetimer_completeevent - Broadcasts when timer hits 0- Analytics stub:
recordSessionToDatabase()
Collaboration:
problem_changed- Updates activeProblemId, broadcasts synccursor_move- Volatile emit for high frequency- Yjs signaling (sync_step_1, sync_step_2, update)
Chat:
send_message- Timestamp, broadcast, persistence stubtyping_start- 3-second TTL in Redistyping_stop- Broadcast
GET /api/rooms- List active public roomsGET /api/rooms?mode=solo- Filter by modePOST /api/rooms- Create room (all 4 modes supported)
curl http://localhost:3000/healthcurl http://localhost:3000/api/rooms
curl "http://localhost:3000/api/rooms?mode=solo"curl -X POST http://localhost:3000/api/rooms \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"mode\":\"solo\",\"isPublic\":true,\"maxCapacity\":5}"powershell -ExecutionPolicy Bypass -File test-api.ps1ping- Heartbeatjoin_room(roomId)- Join with capacity checkleave_room(roomId)- Leave roomtoggle_media({ roomId, type, enabled })- Toggle mic/camtimer_start({ roomId, duration, type })- Start timertimer_pause({ roomId })- Pause timertimer_reset({ roomId })- Reset timerproblem_changed({ roomId, problemId })- Change active problemcursor_move({ roomId, line, column })- Update cursorsend_message({ roomId, message })- Send chattyping_start({ roomId })- Start typingyjs_sync_step_1/2,yjs_update- Yjs CRDT sync
pong- Heartbeat responseroom_state({ roomId, state, users })- Current stateroom_error({ message })- Room full or erroruser_joined({ userId, socketId })- User joineduser_left({ userId, socketId })- User leftmedia_state_changed({ userId, micOn, camOn })- Media updatetimer_sync({ state, endTime, duration, remainingTime })- Timer statetimer_complete({ type, duration })- Timer finishedactive_problem_sync({ problemId, changedBy })- Problem changedcursor_update({ userId, line, column, color })- Cursor movednew_message({ userId, message, timestamp })- New chattyping_indicator({ userId, isTyping })- Typing statusyjs_sync_step_1/2,yjs_update- Yjs sync
server.ts (Entry Point)
├── Express App (REST API)
│ └── /api/rooms (GET, POST)
└── Socket.io Server (WebSocket)
├── Auth Middleware (JWT)
├── Redis Adapter (Scaling)
└── Event Handlers
├── presence.handler.ts
├── timer.handler.ts
├── chat.handler.ts
└── collaboration.handler.ts
server/
├── server.ts # Entry point
├── src/
│ ├── config/
│ │ └── database.ts # Shared Prisma instance
│ ├── services/
│ │ └── redis.service.ts # All Redis operations
│ ├── socket/
│ │ ├── index.ts # Socket.io setup
│ │ ├── auth.middleware.ts # JWT auth
│ │ └── handlers/
│ │ ├── presence.handler.ts # Join/leave/media
│ │ ├── timer.handler.ts # Timer with completion
│ │ ├── chat.handler.ts # Chat with typing TTL
│ │ └── collaboration.handler.ts # Problem sync + Yjs
│ └── modules/
│ └── rooms/
│ ├── roomRoutes.ts
│ └── roomController.ts # All modes + filter
├── .env # Environment config
├── test-api.ps1 # API test script
└── README.md # This file
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/studyroom
JWT_SECRET=your_jwt_secret_key_change_this_in_production
REDIS_HOST=localhost
REDIS_PORT=6379
CLIENT_URL=http://localhost:5173- Check if port 3000 is available
- Verify Redis is running
- Check
.envconfiguration
- Start Redis:
docker-compose -f docker-compose.redis.yml up -d - Or install Redis locally
- Run
npx prisma generate - Run
npm install