Real-time 1v1 competitive coding — LeetCode meets Chess Elo.
Two players. One problem. A live code editor, hidden test cases, and an Elo rating system that updates the moment the match ends. Built end-to-end as a solo project.
- Overview
- Modules
- System Architecture
- Tech Stack
- Database Schema
- Matchmaking Algorithm
- Code Execution Pipeline
- Elo Rating System
- API Reference
- Getting Started
- Environment Variables
- Project Structure
- Roadmap
Coding Duel is a competitive programming platform where players are matched in real-time based on their Elo ratings, assigned a coding problem suited to their skill level, and race to solve it in a shared browser-based IDE. The platform supports multiple win conditions — first to pass all test cases, most test cases solved before time expires, or opponent surrender — mirroring the match dynamics of competitive chess.
The platform is built around 12 modules. Each module handles one clear part of the system and works together with the others to deliver the full experience.
This is the entry point of the platform. Before a user can play any match, they need to create an account and prove who they are.
When a new user signs up, they provide their name, email, and password. The system sends a 6-digit verification code to their email — they must enter this code before their account becomes active. This step ensures that only real, valid email addresses are accepted.
Once verified, the user can log in. The system issues a secure login token (called a JWT — think of it as a digital key) that the user carries with every request they make. The server checks this key to confirm the user is who they claim to be without asking them to log in again on every click.
Key features:
- Account creation with email and password
- Email verification using a one-time code (OTP) that expires after a set time
- Secure login that issues a token, and logout that permanently cancels that token
- Forgot password flow — user requests a reset link, receives a new OTP, and sets a new password
- Protection against banned users — if an admin bans an account, the user cannot log in again
This module is responsible for finding two players who are close in skill level and pairing them into a match — automatically and instantly.
When a player clicks "Find Match," the system looks at their current rating (Elo score) and places them into a waiting group — called a bucket — with other players of a similar rating. Think of it like a queue at a ticket counter, but there are separate counters for beginners, intermediate, and advanced players.
The moment two players end up in the same bucket, the system pairs them, creates a match, and notifies both of them at the same time. If no opponent is found immediately, the player waits in the lobby and can see how many other players are currently searching.
Key features:
- Players are sorted into rating buckets (e.g. 0–99, 100–199, 200+ and so on) so matches are always fair
- The waiting queue is powered by Redis — an ultra-fast in-memory system — so pairing happens in milliseconds
- Players see a live lobby showing estimated wait time and number of active players searching
- Players can cancel matchmaking at any time and leave the queue cleanly
The Elo rating system is a well-established method for measuring player skill. It was originally invented for chess and is now used in games like League of Legends, Chess.com, and competitive programming platforms.
Every new player starts with a base rating of 50. After each match, their rating goes up or down depending on whether they won or lost, and also how strong their opponent was. Beating a stronger opponent earns more points than beating a weaker one.
Key features:
- Ratings update automatically at the end of every match — win, lose, draw, surrender, or time expiry each have their own rules
- A player who surrenders loses rating just like a normal loss — same as resigning in chess
- If a match is mutually aborted (both players agree to cancel), no ratings change
- If the time limit runs out, the player who solved more test cases wins; if it's a tie, it counts as a draw and ratings stay the same
- The system uses the standard Elo formula with a K-factor (a value that controls how much ratings shift per match)
Once two players are matched, the system needs to give them a coding problem to solve. This module handles selecting the right problem for each match — fairly and without repeats.
Problems in the database are tagged by difficulty level — easy, medium, or hard — and these difficulty levels are aligned with the Elo buckets. Players with lower ratings receive easier problems, and higher-rated players receive harder ones. This ensures the challenge is always appropriate for the skill level of the players involved.
Key features:
- Problems are filtered by difficulty based on the players' rating range — beginners do not get hard problems, and advanced players do not get easy ones
- The system checks both players' match history from the past 24 hours and avoids assigning a problem either player has already seen recently — this prevents repetitive matches
- If every suitable problem has been seen recently, the system widens its search until it finds a fresh one
- Both players always receive the exact same problem — the competition is completely symmetric
This module gives players a professional coding environment directly inside the browser — no installation required. Players can write, edit, and submit code without ever leaving the platform.
The editor is built using Monaco Editor, which is the same editor that powers Microsoft Visual Studio Code. Players will find it familiar and comfortable to use.
Key features:
- Supports multiple programming languages — players can choose their preferred language before starting
- Syntax highlighting — keywords, functions, and variables are color-coded to make code easier to read and write
- The layout mirrors LeetCode — the problem statement is on one side, the code editor on the other, so players can read and code at the same time
- The editor includes a submit button that sends the code to the backend for evaluation
When a player submits their code, this module takes over. It sends the code to an external execution engine called Judge0, which runs the code against a series of test cases and reports back whether the solution is correct.
Test cases are pre-written inputs and expected outputs stored for each problem. The system feeds the player's code each input one by one, checks what the code outputs, and compares it to the expected answer.
Key features:
- Code is executed in a completely isolated and sandboxed environment — it cannot affect the server or other players
- Supports all major languages — the same Judge0 engine handles C++, Python, Java, JavaScript, and more
- Results are categorized clearly:
- AC (Accepted) — the code output matched the expected output
- WA (Wrong Answer) — the code ran but gave the wrong result
- RTE (Runtime Error) — the code crashed while running
- CTE (Compile Error) — the code could not even be compiled or interpreted
- TLE (Time Limit Exceeded) — the code took too long to run
- The actual test case inputs and expected outputs are never shown to the player — only the result category and pass/fail count are displayed, exactly like LeetCode
This module keeps both players connected to each other and to the match in real time. Without this module, players would have no idea what their opponent is doing unless they refreshed the page.
It uses a technology called WebSockets, which is a permanent two-way connection between each player's browser and the server. Unlike a normal web request where the browser asks and the server answers, WebSockets allow the server to push information to the browser the moment something happens — no asking required.
Key features:
- The moment a match is found, both players receive a notification at exactly the same time and are directed to the duel room
- When either player submits code and results come back from Judge0, both players see the updated test case status instantly
- The winner is announced live — the moment one player passes all test cases, both players see the result card without any delay
- Each match has its own private room identified by a unique room ID — only the two players in that match are connected to that room, so there is no interference between different matches
Every match that takes place on the platform is saved permanently. This module gives players access to their full history of past matches so they can review their performance, see where they went wrong, and learn from previous duels.
Key features:
- Every completed match is stored in the database with full details — who played, what problem was given, what each player submitted, who won, and what the final rating changes were
- Players can visit their match history page and scroll through past matches sorted by date
- For each match, players can see the final code both they and their opponent submitted — this is the "replay" aspect, letting players compare approaches
- Win, loss, and draw records are tracked over time, giving players a clear picture of how their skill has progressed
The leaderboard is a ranked list of all players on the platform, sorted by their Elo rating. It gives players a sense of where they stand globally and motivates them to keep improving.
Key features:
- A global leaderboard shows all registered players ranked from highest to lowest Elo rating
- Each player can see their exact rank number (e.g. #245 globally) alongside their rating, number of matches played, and win rate
- The leaderboard updates in real time as matches are completed and ratings change
- A seasonal leaderboard resets periodically, giving all players a fresh start and creating a competitive cycle — similar to ranked seasons in games like Valorant or Chess.com
The admin dashboard is a private, restricted area of the platform that only administrators can access. It gives the admin team full visibility and control over everything happening on the platform.
Admins can manage every part of the system from one central place without needing to touch the database directly.
Key features:
- User management — admins can view all registered users, see their activity, and ban accounts that violate platform rules
- Problem management — admins can add new coding problems, update existing ones, delete outdated ones, and add or edit test cases for each problem
- Match oversight — admins can view all matches that have taken place, including who played, what the outcome was, and when it happened
- Platform statistics — a stats page gives admins a high-level overview of total users, total matches played, active sessions, and other key numbers
- Feedback management — all user-submitted feedback appears in the admin panel so the team can review and resolve issues (covered in detail in Module 11)
This module gives users a way to report problems, request features, or ask for help — and gives the admin team the tools to respond and resolve those requests in an organized way.
Rather than users sending emails to a personal inbox or posting on social media, everything goes through a structured form inside the platform. This keeps feedback organized and trackable.
Key features:
- Feedback submission form — users can submit feedback by choosing an issue type (bug, feature request, general inquiry), writing a title and description, and selecting a severity level (low, medium, or high). They can also choose to submit anonymously if they prefer not to share their identity, and optionally attach a screenshot of the issue
- Admin feedback queue — all submitted feedback appears in the admin panel in a list that can be filtered by severity, issue type, or whether it has been resolved yet. This makes it easy for admins to prioritize urgent issues
- Resolution tracking — when an admin resolves a piece of feedback, they mark it as resolved. The admin stats dashboard shows a live count of how many feedback items are open vs. resolved, giving a clear picture of the support workload at any time
This module keeps users informed about what is happening on the platform, both in real time and as a historical record they can look back at.
It has two distinct parts that work differently but serve the same goal — making sure users never miss anything important.
Part A — Real-Time Notifications (the bell icon)
When something significant happens — a match is found, a match result is decided, an Elo change occurs, or feedback is resolved — the user sees an instant notification popup in the top bar of the interface, no matter which page they are currently on. This is powered by the same WebSocket connection used in the duel room, but scoped to the individual user rather than a match room.
Think of it like a phone notification — it appears immediately without the user having to refresh or navigate anywhere.
Part B — Activity Feed (the dashboard card)
Separately from the live notifications, the user's dashboard shows a "Recent Activity" card that lists their last 5–10 actions in chronological order. Examples: "Won a match vs PlayerX (+14 Elo) — 2 hours ago", "Submitted feedback — 1 day ago", "Solved Problem #42 (Easy) — 1 day ago."
Unlike the bell notifications which disappear once dismissed, the activity feed is a permanent log that is always available on the dashboard whenever the user comes back to it.
Key features:
- Real-time bell icon with unread badge count that increments automatically when new events arrive
- Notification dropdown listing recent events with icons and relative timestamps (e.g. "2 min ago")
- "Mark all as read" button to clear the badge count
- Email notifications for important account events — account verified, password reset completed, account banned — sent via the platform's existing email system
- Persistent activity feed on the dashboard built from the match history and feedback tables already stored in the database — no extra storage needed
┌──────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ Next.js 14 (App Router) · Monaco Editor · Zustand │
└──────────┬───────────────────────────────┬───────────────────────┘
│ REST / HTTPS │ WebSocket
┌──────────▼───────────────────────────────▼───────────────────────┐
│ API GATEWAY & AUTH │
│ Express Router · JWT Middleware · WS Server · Rate Limit │
└──────┬───────────┬────────────────────────┬──────────────────────┘
│ │ │
┌──────▼──┐ ┌─────▼──────┐ ┌─────────────▼──────┐ ┌───────────┐
│Matchmak-│ │ Problem │ │ Elo Calculator │ │ Code │
│ing Eng. │ │ Engine │ │ Win/Loss/Draw │ │Submission │
│Elo buck-│ │Elo-filter │ │ K-factor scaling │ │Judge0 Bri-│
│ets·FIFO │ │24h dedup │ │ Surrender logic │ │dge·Verdict│
└──────┬──┘ └─────┬──────┘ └──────────┬─────────┘ └─────┬─────┘
│ │ │ │
┌──────▼───────────▼────────────────────▼───────────────────▼─────┐
│ DATA & INFRASTRUCTURE │
│ Redis 7 (queues · match cache) │
│ PostgreSQL 16 via Drizzle ORM (users · matches · problems) │
│ Docker → Judge0 CE (sandboxed execution) → k8s (prod) │
└──────────────────────────────────────────────────────────────────┘
Player clicks "Find Match"
→ POST /queue-for-match
→ Auth middleware validates JWT
→ Matchmaking Engine reads player Elo
→ Computes bucket key (e.g. "bucket:0-99")
→ RPUSH player data onto Redis queue
→ Poll: LLEN >= 2? → LPOP two entries
→ Problem Engine selects a problem (Elo-filtered, deduped)
→ Match object created (roomID, player1, player2, problem, testCases)
→ Stored in PostgreSQL + Redis (TTL cache)
→ WS Server emits `match:found` to both players on roomID channel
→ Both clients redirect to /duel/[roomID]
Player submits code
→ POST /all-test-check { code, language, problemId }
→ Code Submission service fetches all testCases for problemId
→ Batches { source_code, language_id, stdin } → Judge0 REST API
→ Judge0 runs each case in a sandboxed container
→ Returns per-case: stdout, stderr, status, time, memory
→ Verdict analysis: RTE / CTE / WA / AC, pass count
→ WS Server emits `submission:result` on roomID
→ Both players receive updated test case status
→ If AC (all passed): emit `match:win` / `match:lose`
→ Elo Calculator updates both ratings in PostgreSQL
| Layer | Technology |
|---|---|
| Frontend | Next.js 14, TypeScript, Zustand, Tailwind CSS, Monaco Editor |
| Backend | Node.js, Express.js, TypeScript, ws (WebSockets) |
| Database | PostgreSQL 16, Drizzle ORM |
| Cache / Queue | Redis 7 (FIFO queues per Elo bucket, match object TTL cache) |
| Code Execution | Judge0 Community Edition, Docker (→ Kubernetes in production) |
| Auth | JWT, email OTP verification, session revocation table |
| Column | Type | Notes |
|---|---|---|
id |
uuid PK |
defaultRandom() |
username |
text |
unique |
email |
text |
unique |
password |
text |
hashed |
rating |
integer |
default 50 (starting Elo) |
verified |
boolean |
email verified flag |
banned |
boolean |
admin-controlled |
oauthId |
text |
nullable, for OAuth providers |
codeExpirey |
timestamp |
OTP expiry |
| Column | Type | Notes |
|---|---|---|
id |
serial PK |
|
roomId |
uuid |
unique per match |
problemId |
integer |
FK → problems |
player1 |
jsonb |
player snapshot |
player2 |
jsonb |
player snapshot |
winner |
text |
nullable until resolved |
rated |
boolean |
whether Elo changes apply |
totalCases |
integer |
total test cases in problem |
solution |
jsonb |
both players' final code |
status |
text |
ongoing | completed | draw |
| Column | Type | Notes |
|---|---|---|
id |
serial PK |
|
problemId |
integer |
unique display ID |
title |
text |
|
statement |
text |
problem description |
input / output |
text |
format description |
constraints |
text |
|
testCases |
jsonb |
array of { input, output } |
Tracks active JWT sessions with revocation support (revoked, expiresAt).
Stores marathon session data: problems (JSONB array of solved problems), totalTime, status.
User-submitted feedback with issueType, severity, anonymous flag, and resolved status.
| Column | Type | Notes |
|---|---|---|
id |
serial PK |
|
userId |
uuid |
FK → users |
type |
text |
match_found | match_won | match_lost | feedback_resolved | account_event |
title |
text |
display text |
read |
boolean |
default false |
createdAt |
timestamp |
defaultNow() |
Players are assigned to Redis queues based on Elo bucket ranges:
| Bucket | Elo Range | Problem Difficulty |
|---|---|---|
bucket:0 |
0 – 99 | Easy |
bucket:1 |
100 – 199 | Easy–Medium |
bucket:2 |
200 – 349 | Medium |
bucket:3 |
350 – 499 | Medium–Hard |
bucket:4 |
500+ | Hard |
queueForMatch(player):
bucketKey = getBucket(player.rating)
RPUSH bucketKey serialize(player)
if LLEN(bucketKey) >= 2:
p1 = LPOP(bucketKey)
p2 = LPOP(bucketKey)
problem = selectProblem(bucketKey, [p1.history, p2.history])
createMatch(p1, p2, problem)
else:
wait (show lobby with estimated wait + active players)
Problem deduplication: If either player solved the candidate problem in the last 24 hours, the engine re-samples from the pool until a fresh problem is found.
Client → POST /all-test-check
{ code: string, language: string, problemId: number }
Backend:
1. Fetch all testCases WHERE problemId = ?
2. For each { input, expectedOutput }:
POST judge0/submissions { source_code, language_id, stdin }
3. Poll for results (or use callbacks)
4. Analyze verdict array:
- All AC → match win
- Any RTE / CTE → surface error type (not raw stderr)
- Count passed / total → emit partial progress
WebSocket:
emit('submission:result', { passed, total, verdict, time, memory })
if allPassed: emit('match:win') to winner, emit('match:lose') to opponent
Test case inputs/outputs are never sent to the client — only pass/fail counts and error categories are exposed, matching competitive platform standards.
Rating changes use the standard Elo formula with a fixed K-factor:
expected = 1 / (1 + 10^((opponentRating - playerRating) / 400))
newRating = currentRating + K * (actual - expected)
Where:
actual = 1.0 (win)
= 0.5 (draw / time-expiry tie)
= 0.0 (loss / surrender)
K = 32 (adjustable per tier)
Special cases:
- Abort (mutual): no Elo change for either player
- Time-expiry: player with more test cases passed wins; tie → draw
- Surrender: treated as a loss for the surrendering player
| Method | Endpoint | Description |
|---|---|---|
| POST | /signin |
Sign in, returns JWT |
| POST | /create-account |
Register new user |
| POST | /forget-password |
Send OTP to email |
| POST | /reset-password |
Reset with OTP |
| POST | /verify-account |
Email verification |
| POST | /logout-account |
Revoke session |
| Method | Endpoint | Description |
|---|---|---|
| POST | /get-stats |
Player stats |
| POST | /queue-for-match |
Enter matchmaking |
| POST | /cancel-matchmaking |
Dequeue player |
| POST | /time-expiry |
Trigger time-expiry resolution |
| POST | /notify-passed-all |
Signal all test cases passed |
| POST | /notify-loss |
Signal match loss |
| POST | /match-abort |
Abort current match |
| POST | /match-rankings |
Leaderboard data |
| POST | /match-history |
Past matches for player |
| Method | Endpoint | Description |
|---|---|---|
| POST | /all-test-check |
Submit code, run all test cases |
| POST | /problem |
Fetch problem by ID |
| Method | Endpoint | Description |
|---|---|---|
| GET | /admin-stats |
Platform-wide statistics |
| GET | /admin-users |
All users |
| GET | /admin-feedbacks |
All feedback |
| GET | /admin-matches |
All matches |
| GET | /admin-problems |
All problems |
| POST | /admin-user-activity |
User activity log |
| POST | /admin-feedback-activity |
Feedback activity |
| POST | /admin-add-problem |
Add problem |
| POST | /admin-delete-problem |
Delete problem |
| POST | /admin-update_problem |
Update problem |
| POST | /admin-add-testCase |
Add test case to problem |
| Method | Endpoint | Description |
|---|---|---|
| POST | /add-feedback |
Submit user feedback |
| GET | /admin-feedbacks |
List all feedback (admin) |
| POST | /admin-feedback-activity |
Feedback activity log (admin) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /notifications |
Paginated notifications for current user |
| POST | /notifications/mark-read |
Mark all notifications as read |
| GET | /activity |
Recent activity feed for dashboard |
- Node.js 20+
- PostgreSQL 16
- Redis 7
- Docker (for Judge0)
git clone https://github.com/your-username/coding-duel-platform.git
cd coding-duel-platformcd judge0
docker compose up -dJudge0 will be available at http://localhost:2358 by default.
# Backend
cd server
npm install
# Frontend
cd ../client
npm installSee Environment Variables below.
cd server
npx drizzle-kit push# Backend (from /server)
npm run dev
# Frontend (from /client)
npm run dev# Database
DATABASE_URL=postgresql://user:password@localhost:5432/coding_duel
# Redis
REDIS_URL=redis://localhost:6379
# Auth
JWT_SECRET=your_jwt_secret_here
JWT_EXPIRY=7d
# Judge0
JUDGE0_BASE_URL=http://localhost:2358
JUDGE0_AUTH_TOKEN= # leave empty for local CE
# Email (SMTP)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your@email.com
SMTP_PASS=your_smtp_password
# Server
PORT=4000
NODE_ENV=developmentNEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_WS_URL=ws://localhost:4000coding-duel-platform/
├── client/ # Next.js frontend
│ ├── app/ # App Router pages
│ │ ├── (auth)/ # Login, register, verify
│ │ ├── dashboard/ # Main player dashboard
│ │ ├── duel/[roomId]/ # Live duel room
│ │ ├── leaderboard/ # Rankings
│ │ ├── marathon/ # Marathon mode
│ │ └── admin/ # Admin panel
│ ├── components/ # Shared UI components
│ ├── store/ # Zustand state slices
│ └── lib/ # API client, WS helpers
│
├── server/ # Express backend
│ ├── controllers/
│ │ ├── authController.ts
│ │ ├── matchController.ts
│ │ ├── codeCheck_Controller.ts
│ │ ├── generalController.ts
│ │ └── AdminController.ts
│ ├── routes/ # Express router
│ ├── db/
│ │ ├── schema.ts # Drizzle schema definitions
│ │ └── index.ts # DB connection
│ ├── services/
│ │ ├── matchmaking.ts # Redis queue logic
│ │ ├── elo.ts # Rating calculation
│ │ ├── judge0.ts # Judge0 API client
│ │ └── websocket.ts # WS room management
│ └── middleware/
│ ├── auth.ts # JWT validation
│ └── rateLimiter.ts
│
└── judge0/ # Docker Compose for Judge0
└── docker-compose.yml
- Spectator mode — watch live matches in real time
- Custom room creation — private duels with invite links
- Team mode — 2v2 collaborative solving
- Problem submission portal — community-contributed problems with admin review
- Kubernetes deployment manifests for Judge0 horizontal scaling
- OAuth (GitHub / Google) login
- Mobile-responsive duel room layout
Salman Ahmed Khan
Software Engineer · SZABIST Islamabad, Class of 2026
LinkedIn · GitHub
Built with Node.js · Next.js · Redis · PostgreSQL · Judge0 · Docker