Communication problems (run twice) are supported as a separate problem type. See the configuration guide and encoding/decoding example.
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.
- Cached checker, interactor and communicator compilation, automatically rebuilt
when the source,
testlib.hor compilation options change. - 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.
In the admin problem bank, click the target row's Update Problem upload icon, select an exported ZIP containing exactly one problem, then click Update Problem. Only one ZIP is accepted per update; archives with zero or multiple problems are rejected. The update keeps the target ID, contest links and submissions while replacing its statement, samples, limits, judge configuration, test data and assets, including removing obsolete files. Existing submissions are not automatically rejudged. The regular Import action still creates new problems.
Run regression tests with npm run test:problem-update.
With the local service running, use npm run test:problem-update:integration for real HTTP uploads.
It creates and cleans up temporary problems, contests and an administrator, saving reports and ZIPs
under .local/problem-update-qa/. Set PROBLEM_UPDATE_TEST_URL to override the service URL.
Private contests require a contest password. Every visitor, including signed-in team and global accounts, must verify it before viewing contest details, problems, standings, submissions, notices, editorials or private-only assets. This also applies after the contest ends. Super administrators retain access. The contest list and training center show only a locked contest ID until it is unlocked; unlocking grants no extra submission, source-code or staff privileges.
Browser grants expire after 24 hours and are cleared on logout. Changing the
contest password invalidates existing grants. Legacy private contests without
a password deny access until an administrator configures one. CLI login takes
contestPassword and returns contestAccessToken, sent on subsequent requests
as x-contest-access-token. The Node.js client prompts for it; the PowerShell
client reads contestPassword from its desktop credentials file. CCS Basic
authentication is restricted to super administrators.
Run access regression tests with npm run test:contest-access.
- 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"
JUDGE_WALL_WATCHDOG_MS=60000 # Minimum elapsed-time watchdog; not the contestant CPU budgetContestant TLE is based on measured CPU time. Scheduling and I/O waits do not
consume that budget. Every contestant run has a separate elapsed-time watchdog
of max(JUDGE_WALL_WATCHDOG_MS, 20 × CPU limit) to release stuck executions.
If the watchdog expires without a measured CPU overrun, the submission receives
SYSTEM_ERROR with CPU/elapsed timing diagnostics, not a contestant penalty.
The CPU limit and existing language multipliers are unchanged. Restart the Web
and Worker processes after updating the code or watchdog configuration.
Run npm run test:judge-runtime for regression checks and
npm run test:judge-runtime:integration against go-judge for real PyPy waiting,
CPU exhaustion and watchdog checks. The integration checks use disposable
programs and do not modify submissions or the database.
- 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.
Compiled checkers, interactors and communicators are saved in
uploads/problems/<problem ID>/.judge-cache/. Processes sharing this directory
and the same go-judge URL reuse the compiled bytes. Source paths, source contents,
testlib.h and compilation options are checked on each request; changes or a
missing/corrupt cache trigger compilation. Redis leases coordinate concurrent
cache misses across worker processes.
Each submission uploads its own temporary executable from the cached bytes and cleans it up after judging. Updating a source does not affect submissions already using an older executable, and cached bytes survive Web, Worker and go-judge restarts. The cache lives outside the test data directory, so data uploads do not replace it and problem exports do not include it. Remove the corresponding cache directory after upgrading the compiler or sandbox environment to force a rebuild.
Run npm run test:judge-cache for cache tests. With Redis and go-judge available,
run npm run test:judge-cache:integration for real compilation and multi-process
checks without database access.
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.