A self-hosted competitive programming platform built for institutions.
Host timed contests · Judge submissions in real-time · Rank participants live
Overview · Features · Architecture · Get Started · Database · API · Roadmap
VRDCT is a full-stack competitive programming platform for colleges, bootcamps, and coding clubs. Unlike public platforms — you own everything. Your data, your contests, your judge pipeline.
Register → Join Contest → Write Code → Get Judged → See Leaderboard
Two question modes, real-time verdict, partial scoring, and a live leaderboard with podium highlights for top 3.
🎓 For Students
| Feature | Details | |
|---|---|---|
| 📋 | Registration Modal | Name, class, roll number — blurred backdrop popup |
| ⏱️ | Live Countdown | Contest timer, red pulse animation in final 2 minutes |
| 🖊️ | Monaco Editor | VS Code-grade editor — Python, Java, C++ |
| 🐛 | Bug Fix Mode | Admin-provided buggy code pre-loaded per language |
| 💾 | Code Persistence | Code saved per question + per language in localStorage |
| Custom Test Runner | Run against custom stdin before final submit | |
| 🏁 | Submit All | One-click final submission — locks score, shows results |
| 📊 | Personal Results | Partial credit, progress bars, accuracy breakdown |
| 🏆 | Leaderboard | Full rankings with gold / silver / bronze podium |
🛠️ For Admins
| Feature | Details | |
|---|---|---|
| ⚡ | Inline Contest Builder | Create contest + questions + test cases in a single form |
| 🐛 | Bug Fix Questions | Per-language buggy starter code per question |
| 🧪 | Test Case Builder | Add/remove cases, mark samples, side-by-side I/O |
| 🖼️ | Banner Upload | Cloudinary image upload directly from the browser |
| 🕐 | TZ-Aware Scheduling | Times stored with offset — no UTC shift bugs |
| 📝 | Draft Mode | Build contests privately, publish when ready |
| 🎯 | Contest Dashboard | Live / upcoming / ended status at a glance |
graph TD
User(["👤 User / Admin"])
subgraph Frontend ["Frontend — React 18 + Vite"]
UI["Pages & Components"]
SC["Supabase Client"]
AX["Axios API Client"]
end
subgraph Backend ["Backend — Node.js + Express"]
AUTH["Auth Controller\n/register /login"]
JUDGE["Judge Controller\n/run /submit /finalize"]
MW["JWT Middleware"]
end
subgraph Services ["External Services"]
DB[("Supabase\nPostgreSQL")]
SAND["Code Sandbox\ncompile → run → cleanup"]
CDN["Cloudinary\nImage Storage"]
end
User --> UI
UI --> SC
UI --> AX
SC --> DB
AX --> MW --> AUTH
AX --> MW --> JUDGE
AUTH --> DB
JUDGE --> DB
JUDGE --> SAND
UI --> CDN
style User fill:#1a1a2e,stroke:#6c63ff,color:#fff
style Frontend fill:#0f1b35,stroke:#6c63ff,color:#ccc
style Backend fill:#0f1b35,stroke:#6c63ff,color:#ccc
style Services fill:#0f1b35,stroke:#14a085,color:#ccc
style DB fill:#0d4f4f,stroke:#14a085,color:#fff
style SAND fill:#2d1b69,stroke:#6c63ff,color:#fff
style CDN fill:#1a2744,stroke:#3448C5,color:#fff
sequenceDiagram
actor S as Student
participant UI as React UI
participant API as Express API
participant DB as PostgreSQL
participant J as Code Sandbox
S->>UI: Click Submit
UI->>API: POST /submit/:questionId
API->>DB: Check registration
API->>DB: Check contest is live
API->>DB: Fetch test cases
API->>J: Compile code (once)
loop Each test case
J->>J: Run with input
J-->>API: output + timeMs
end
API->>DB: Save submission + verdict
API-->>UI: verdict · passedCount · totalCount
UI-->>S: Show result
stateDiagram-v2
direction LR
[*] --> Draft : Admin creates
Draft --> Upcoming : Published
Upcoming --> Live : start_time reached
Live --> Finalized : Student submits all
Live --> Ended : end_time reached
Finalized --> Ended : end_time reached
Ended --> [*]
| Layer | Technology |
|---|---|
| Frontend | React 18, Vite, TailwindCSS, React Router v6 |
| Editor | Monaco Editor (@monaco-editor/react) |
| Backend | Node.js, Express.js |
| Database | Supabase (PostgreSQL) |
| Auth | Custom JWT stored in localStorage |
| Media | Cloudinary — unsigned upload preset |
| Code Execution | Custom sandbox — compile once, run per test case |
| Code Persistence | localStorage keyed by contest · questionId · lang |
vrdct/
├── frontend/
│ └── src/
│ ├── pages/
│ │ ├── Contests.jsx ← listing + registration modal
│ │ ├── ContestDetail.jsx ← countdown + side nav + editor
│ │ ├── ProblemSolvingView.jsx ← Monaco + judge + localStorage
│ │ ├── Leaderboard.jsx ← podium + full ranked table
│ │ └── AdminDashboard.jsx ← contest + question builder
│ ├── context/
│ │ └── AuthContext.jsx ← JWT auth state
│ └── utils/
│ ├── time.js ← timezone-safe parsing
│ └── uploadToCloudinary.js ← browser-side upload
│
└── backend/
├── controllers/
│ ├── authController.js
│ └── judgeController.js ← run · create · submit · finalize
├── middleware/
│ └── authMiddleware.js ← verifyToken · verifyAdmin
├── utils/
│ ├── judge.js ← sandbox pipeline
│ └── verdict.js ← checkAllTestCases
└── routes/
├── auth.js
└── judge.js
node >= 18 npm >= 9
Accounts: Supabase · Cloudinary
git clone https://github.com/yourusername/vrdct.git
cd vrdctcd backend && npm installbackend/.env
PORT=5000
JWT_SECRET=your_jwt_secret
DB_HOST=db.xxxxxxxxxxxx.supabase.co
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASSWORD=your_db_passwordcd frontend && npm installfrontend/.env
VITE_API_URL=http://localhost:5000
VITE_SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=your_anon_key
VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name
VITE_CLOUDINARY_UPLOAD_PRESET=contest_imagesRun the schema below in your Supabase SQL editor.
UPDATE users SET role = 'admin' WHERE email = 'your@email.com';# Terminal 1 — backend
cd backend && npm run dev
# Terminal 2 — frontend
cd frontend && npm run devOpen http://localhost:5173
Click to expand full SQL schema
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'student' CHECK (role IN ('student','admin')),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE contests (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
image_url TEXT,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
created_by UUID REFERENCES users(id),
is_published BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE contest_questions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
contest_id UUID REFERENCES contests(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
question_type VARCHAR(20) DEFAULT 'normal'
CHECK (question_type IN ('normal','bug_fix')),
starter_code JSONB, -- { "python": "...", "java": "...", "cpp": "..." }
time_limit_ms INTEGER DEFAULT 2000,
order_index INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE contest_test_cases (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
question_id UUID REFERENCES contest_questions(id) ON DELETE CASCADE,
input TEXT DEFAULT '',
expected_output TEXT NOT NULL,
is_sample BOOLEAN DEFAULT false,
order_index INTEGER DEFAULT 0
);
CREATE TABLE contest_registrations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
contest_id UUID REFERENCES contests(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
full_name VARCHAR(255) NOT NULL,
class VARCHAR(100) NOT NULL,
roll_no VARCHAR(50) NOT NULL,
registered_at TIMESTAMP DEFAULT NOW(),
final_score INTEGER DEFAULT 0,
submitted_at TIMESTAMP,
UNIQUE (contest_id, user_id)
);
CREATE TABLE submissions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
question_id UUID REFERENCES contest_questions(id) ON DELETE CASCADE,
contest_id UUID REFERENCES contests(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
language VARCHAR(20) NOT NULL
CHECK (language IN ('python','java','cpp')),
code TEXT NOT NULL,
verdict VARCHAR(30) NOT NULL,
passed_count INTEGER DEFAULT 0,
total_count INTEGER DEFAULT 0,
time_ms INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);| Method | Endpoint | Description |
|---|---|---|
POST |
/api/auth/register |
Register new user |
POST |
/api/auth/login |
Login — returns JWT |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/judge/run |
Token | Run code against custom input |
POST |
/api/judge/contest |
Admin | Create contest + questions in one transaction |
POST |
/api/judge/submit/:questionId |
Token | Submit + judge code |
POST |
/api/judge/contest/:contestId/finalize |
Token | Lock final score |
GET |
/api/judge/submission/:id |
Token | Fetch a submission |
Submit — request & response
Request
POST /api/judge/submit/:questionId
{
"code": "print(int(input()) * 2)",
"language": "python",
"contestId": "uuid-here"
}Response
{
"submissionId": "uuid",
"finalVerdict": "AC",
"passedCount": 5,
"totalCount": 5,
"testCaseDetails": [
{ "testCase": 1, "verdict": "AC", "timeMs": 42 },
{ "testCase": 2, "verdict": "AC", "timeMs": 38 }
]
}| Code | Meaning |
|---|---|
AC |
Accepted — all test cases passed |
WA |
Wrong Answer |
TLE |
Time Limit Exceeded |
CE |
Compile Error |
RE |
Runtime Error |
Each question → 100 pts max
Partial credit → floor( passed / total × 100 )
Final score → best submission per question, summed
Tiebreaker → earlier submission time wins
| Student | Q1 | Q2 | Q3 | Total | Rank |
|---|---|---|---|---|---|
| Arjun | 100 | 80 | 100 | 280 | 🥇 |
| Priya | 100 | 100 | 60 | 260 | 🥈 |
| Rahul | 80 | 60 | 100 | 240 | 🥉 |
Why contest-scoped questions instead of a global problem bank?
Each contest owns its questions. No pollution between contests, no accidental reuse, simpler permission model.
Why localStorage for code persistence?
Monaco remounts when switching questions. localStorage gives instant restoration per
contestId + questionId + language — zero API calls, zero latency.
Why JSONB for starter_code?
Bug Fix mode needs different buggy code per language. A single JSONB column
{ "python": "", "java": "", "cpp": "" } handles this cleanly without extra tables.
Why TIMESTAMPTZ instead of TIMESTAMP?
TIMESTAMPTZ compares correctly against NOW() (UTC) regardless of how the client generated the string. This makes the live-check query — start_time <= NOW() AND end_time >= NOW() — reliable across all timezones.
- Realtime leaderboard via Supabase subscriptions
- Plagiarism detection — cross-submission similarity scoring
- ICPC mode — penalty time scoring
- Admin analytics — pass rate and time distribution per question
- Per-student submission history
- Export leaderboard as CSV / PDF
- Email notifications for contest start
- Mobile-responsive editor
git checkout -b feature/your-feature
git commit -m "feat: describe your change"
git push origin feature/your-feature
# Open a Pull RequestCommit convention: feat · fix · refactor · docs · style
MIT — use it, ship it, build on it.