A production-ready, horizontally-scalable real-time quiz contest application built with Go, WebSockets, Redis Pub/Sub, and PostgreSQL. some fixes
- Real-time Multiplayer Contests: Up to 6 players can compete simultaneously
- Synchronized Gameplay: All players see questions at the same time
- Instant Question Progression: When one player answers correctly, everyone moves to the next question
- Speed-Based Scoring: Faster correct answers earn more points (up to 1000 points per question)
- Horizontal Scalability: Redis Pub/Sub enables multiple server instances
- JWT Authentication: Secure authentication for all API endpoints
- Graceful Shutdown: Properly closes all connections and saves data
- WebSocket Communication: Low-latency, bidirectional communication
momentum-contest/
โโโ cmd/
โ โโโ server/
โ โโโ main.go # Application entry point
โโโ internal/
โ โโโ api/
โ โ โโโ auth.go # JWT authentication & middleware
โ โ โโโ router.go # HTTP routes & handlers
โ โโโ config/
โ โ โโโ config.go # Configuration management
โ โโโ contest/
โ โ โโโ client.go # WebSocket client management
โ โ โโโ hub.go # Contest room logic
โ โ โโโ manager.go # Multi-room management
โ โ โโโ message.go # Message protocol definitions
โ โ โโโ pubsub.go # Redis Pub/Sub integration
โ โโโ storage/
โ โโโ postgres.go # Database operations
โโโ go.mod
โโโ go.sum
โโโ README.md
โโโ schema.sql # Database schema
git clone <repository-url>
cd momentum-contestgo mod downloadCreate a database and run the schema:
# Connect to PostgreSQL
psql -U postgres
# Create database
CREATE DATABASE contest_db;
# Exit psql
\q
# Run the schema
psql -U postgres -d contest_db -f schema.sqlStart Redis server:
# Using Docker (recommended)
docker run -d -p 6379:6379 redis:latest
# Or install locally and run
redis-serverCreate a .env file or set environment variables:
# Server Configuration
PORT=8080
# Database Configuration
DATABASE_URL=postgres://postgres:postgres@localhost:5432/contest_db?sslmode=disable
# Redis Configuration
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
# JWT Configuration (CHANGE IN PRODUCTION!)
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# Game Configuration
QUESTION_TIMER=15 # Seconds per question
MAX_PLAYERS=6 # Maximum players per contest# Build
go build -o server ./cmd/server
# Run
./server
# Or run directly
go run ./cmd/server/main.goThe server will start on http://localhost:8080
POST /login
Content-Type: application/json
{
"username": "player1",
"password": "any-password"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user_id": "user_player1_1234567890",
"username": "player1",
"expires_at": "2025-11-03T12:00:00Z"
}POST /api/contests
Authorization: Bearer <token>
Content-Type: application/json
{
"difficulty": "medium",
"question_count": 10
}Response:
{
"contest_id": "550e8400-e29b-41d4-a716-446655440000",
"difficulty": "medium",
"question_count": 10,
"websocket_url": "/ws/contests/550e8400-e29b-41d4-a716-446655440000",
"message": "Contest created successfully. Connect to the WebSocket to join."
}GET /healthResponse:
{
"status": "healthy",
"active_contests": 5
}ws://localhost:8080/ws/contests/{contestID}?token={jwt_token}
{
"type": "START_GAME"
}{
"type": "SUBMIT_ANSWER",
"question_id": "q123",
"answer": "Option A"
}{
"type": "PLAYER_JOINED",
"payload": {
"user_id": "user123",
"username": "player1",
"is_host": false,
"player_count": 3,
"players": [...]
}
}{
"type": "CONTEST_STARTED",
"payload": {
"message": "Contest has started! Good luck!",
"total_questions": 10,
"question_timer": 15,
"players": [...]
}
}{
"type": "NEW_QUESTION",
"payload": {
"question_number": 1,
"total_questions": 10,
"question_id": "q123",
"question_text": "What is the capital of France?",
"options": ["London", "Paris", "Berlin", "Madrid"],
"timer": 15
}
}{
"type": "ANSWER_RESULT",
"payload": {
"question_id": "q123",
"is_correct": true,
"correct_answer": "Paris",
"points_awarded": 850,
"time_taken": 2.5,
"new_score": 1700
}
}{
"type": "SCORE_UPDATE",
"payload": {
"user_id": "user123",
"username": "player1",
"score": 1700,
"points_earned": 850
}
}{
"type": "GAME_OVER",
"payload": {
"message": "Contest finished! Here are the final results.",
"final_scoreboard": [
{
"user_id": "user123",
"username": "player1",
"score": 8500,
"is_host": true,
"rank": 1
}
],
"questions": [...]
}
}- Contest Creation: Creator calls
/api/conteststo create a room - Players Join: All players (including creator) connect via WebSocket
- Start Game: Host sends
START_GAMEmessage - Question Loop:
- Server sends question to all players
- 15-second timer starts
- Players submit answers
- First correct answer: Award points, broadcast update, next question
- Timer expires: No points, next question
- Game End: After final question, server sends results and saves to DB
- JWT Authentication: All endpoints require valid JWT tokens
- Token Validation: Tokens validated via middleware
- WebSocket Security: Token required in query parameter or header
- CORS Protection: Configurable origin checking (set properly in production)
The application uses Redis Pub/Sub to synchronize state across multiple server instances:
- Each server instance subscribes to
contest:*pattern - When an event occurs, it's published to Redis
- All server instances receive the event and update their local clients
- This allows contests to span multiple servers
# Terminal 1
PORT=8080 go run ./cmd/server/main.go
# Terminal 2
PORT=8081 go run ./cmd/server/main.go
# Use a load balancer (nginx, HAProxy) to distribute trafficThe application requires three main tables:
- questions: Stores quiz questions with options and correct answers
- contests: Stores contest metadata
- contest_results: Stores final player scores and rankings
- player_answers: Stores individual answer submissions (bulk inserted)
See schema.sql for the complete schema.
We provide three automated test scripts for easy testing:
# Install dependencies
npm install -g wscat ws
# Run interactive mode
node test_contest.js
# Or run auto-play mode directly
node test_contest.js --auto
# Or join existing contest
node test_contest.js <contest-id>Features:
- โ Interactive menu for test scenarios
- โ Auto-play mode with AI players
- โ Colored console output
- โ Automatic answer submission
- โ Real-time score tracking
# Install dependencies
pip install websockets requests
# Run interactive mode
python test_contest.py
# Or run auto-play mode
python test_contest.py --auto
# Or join existing contest
python test_contest.py <contest-id>Features:
- โ Async/await support
- โ Clean output with emojis
- โ Multiple AI players
- โ Easy to extend
# Make executable
chmod +x test_contest.sh
# Run full test setup
./test_contest.sh
# Create a contest
./test_contest.sh create TestHost medium 5
# Connect to existing contest
./test_contest.sh connect <contest-id> TestUserFeatures:
- โ No dependencies (except curl and wscat)
- โ Creates multiple players automatically
- โ Saves tokens for manual testing
- โ Provides connection commands
# 1. Login
TOKEN=$(curl -s -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"test"}' \
| jq -r '.token')
# 2. Create Contest
CONTEST=$(curl -s -X POST http://localhost:8080/api/contests \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"difficulty":"medium","question_count":5}')
echo $CONTEST
# 3. Extract Contest ID
CONTEST_ID=$(echo $CONTEST | grep -o '"contest_id":"[^"]*"' | cut -d'"' -f4)
# 4. Connect via WebSocket
wscat -c "ws://localhost:8080/ws/contests/$CONTEST_ID?token=$TOKEN"Use a WebSocket client like wscat:
# Install wscat
npm install -g wscat
# Connect (use actual values, not placeholders!)
wscat -c "ws://localhost:8080/ws/contests/$CONTEST_ID?token=$TOKEN"
# Send start game (if you're the host)
{"type":"START_GAME"}
# Submit answer
{"type":"SUBMIT_ANSWER","question_id":"q123","answer":"Paris"}The automated scripts support several test scenarios:
-
Basic Flow (3 players, manual control)
- Creates host and 2 additional players
- Manual answer submission
- Good for debugging and understanding flow
-
Auto-Play (4 AI players, automatic)
- All players auto-answer questions
- Simulates real game conditions
- Great for performance testing
-
Join Existing (single player)
- Join an already created contest
- Good for testing late-joining behavior
- Ensure PostgreSQL is running
- Verify DATABASE_URL is correct
- Check firewall/network settings
- Ensure Redis is running:
redis-cli pingshould returnPONG - Verify REDIS_ADDR is correct
- Ensure JWT token is valid and not expired
- Check that token is passed in query parameter
- Verify contest exists before connecting
- Change
JWT_SECRETto a strong random value - Configure proper CORS origins in
router.go - Use environment variables for all sensitive data
- Enable PostgreSQL SSL mode
- Set up Redis authentication
- Configure load balancer for multiple instances
- Set up monitoring and logging
- Implement rate limiting
- Add database migrations tool
- Configure proper user authentication (replace dummy login)
- Set up HTTPS/TLS certificates
- Implement database connection pooling tuning
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
This project is licensed under the MIT License.
Built with โค๏ธ using Go, WebSockets, Redis, and PostgreSQL
Happy Coding! ๐