NovaJudge is a lightweight Online Judge platform designed for XCPC-style training and onsite contests. It focuses on contest operations rather than only standalone submissions: problem management, team accounts, realtime judging, scoreboards, clarifications, balloon delivery, print queues, API keys, and ICPC CCS-compatible data feeds are built into one Next.js application.
- Contest lifecycle management for public and private contests.
- Team account import, login, roles, seats, members, schools, coaches, and categories.
- Problem bank with Markdown/LaTeX statements, samples, assets, test data, and YAML judge configuration.
- Realtime submissions powered by Redis, BullMQ, worker processes, and go-judge.
- Multiple judge workers with unique IDs, live status on the admin dashboard, and conflict-free task claiming from the shared queue.
- ACM/ICPC-style scoreboard with freeze and unfreeze support.
- Clarification and notice system for contestants and judges.
- Balloon delivery and print queue workflows for onsite contests.
- API key authentication and ICPC CCS-compatible endpoints for external tools.
- Training center and virtual participation support for logged-in global users.
- Next.js 16, React 19, TypeScript
- Tailwind CSS, Heroicons, Sonner, Monaco Editor
- PostgreSQL, Prisma 7
- Redis, BullMQ
- go-judge
- Docker Compose for local infrastructure
Clone the repository and install dependencies:
git clone https://github.com/pppolf/NovaJudge.git
cd NovaJudge
npm installCreate an environment file:
cp .env.example .envCommon environment variables:
DATABASE_URL="postgresql://postgres:password@localhost:5432/xcpc_oj?schema=public"
REDIS_URL="redis://localhost:6379"
GO_JUDGE_API="http://localhost:5050"
JWT_SECRET="please-change-this-secret"
SUPER_ADMIN_USERNAME="admin"
SUPER_ADMIN_PASSWORD="123456"
JUDGE_CONCURRENCY=4
NOVAJUDGE_URL="http://localhost:3001"Start PostgreSQL, Redis, and go-judge:
docker-compose up -dInitialize Prisma and start the web app:
npx prisma db push
npm run prisma
npm run devStart the judge worker in another terminal:
npm run workerThe default development server runs at http://localhost:3001.
All judge workers share the same Redis-backed BullMQ queue (judge-queue).
BullMQ claims each job atomically, so no matter how many workers are running,
a submission is only ever picked up and judged by exactly one worker — they
never conflict with each other.
# Start a single worker (ID is auto-assigned, e.g. MacBook-1)
npm run worker
# Start a worker with an explicit ID
npm run worker -- --id 1
npm run worker -- --id 2
# Start several workers at once: 4 workers with IDs 1-4
npm run worker:multi -- 4
# Or control it through environment variables
JUDGE_WORKER_COUNT=4 npm run worker:multi
JUDGE_WORKER_IDS=1,2,3 npm run worker:multiRelated environment variables:
JUDGE_CONCURRENCY=4 # How many submissions each worker judges in parallel
# (total capacity = workers x this value)
JUDGE_ID=1 # Worker ID (optional; auto-assigned when not set)
JUDGE_WORKER_COUNT=4 # Worker count for "npm run worker:multi"
JUDGE_WORKER_IDS=1,2 # Explicit ID list for "npm run worker:multi"- Atomic claiming — BullMQ locks each job in Redis when it is claimed, so two workers can never pick up the same job at the same time.
- Idempotent enqueueing — every code path enqueues submissions through
enqueueJudge()inlib/queue.ts, which uses a fixed job ID per submission. Repeated clicks or concurrent rejudges produce only one queue job, and a submission that is already being judged is never enqueued a second time. - Single scheduler — the contest status scheduler
(
PENDING → RUNNING → ENDED) elects one leader among the workers through a Redis lease, so it never runs twice.
stress-test.ts pushes submissions into the judge queue in batches and
measures judging throughput across all your workers:
npm run stress -- --total=100 --batch=20 --lang=cppOptions: --total (submission count), --batch (batch size), --lang
(c | cpp | java | pypy3), --problem (problem ID),
--no-wait (enqueue only, don't wait for results), --timeout (seconds
to wait for judging to finish), and --mark (a marker comment written into
every submission for later identification). It reports enqueue throughput,
verdict distribution, total judging time, and how many submissions each worker
processed.
Every worker registers itself in Redis with a heartbeat (ID, hostname, PID, concurrency, processed count, currently judging submissions). The admin dashboard (Admin → Dashboard → Judge Workers) lists all online workers with their IDs plus queue statistics (waiting / active / done / failed) and refreshes automatically every 5 seconds. A worker that stops is removed from the list within 15 seconds.
npm run dev # Start the development server
npm run build # Build for production
npm run start # Start the production server
npm run lint # Run ESLint
npm run prisma # Generate Prisma Client
npm run worker # Start a judge worker (auto-assigned ID)
npm run worker:multi # Start multiple judge workers, e.g. "npm run worker:multi -- 4"
npm run worker:dev # Start a judge worker in watch mode
npm run stress # Stress-test the judge queue with bulk submissionsNovaJudge/
├─ app/ # Next.js App Router pages and API routes
├─ components/ # Shared UI components
├─ context/ # Frontend providers
├─ docs/ # API documentation and notes
├─ lib/ # Auth, judge, queue, Redis, Prisma, CCS helpers
├─ prisma/ # Database schema and migrations
├─ public/ # Static assets
├─ scripts/ # CLI and maintenance scripts
├─ uploads/ # Problem data and assets
├─ worker.ts # Judge worker (multi-instance with heartbeat)
└─ docker-compose.yml
NovaJudge exposes platform APIs for contest/problem/submission workflows and ICPC CCS-compatible endpoints for contest tooling, resolver workflows, and onsite displays. API key authentication is available for trusted integrations.
See docs/API_DOCS.md for more details.
NovaJudge is released under the MIT License.