A complete online judge in the spirit of Codeforces, LightOJ, SPOJ and UVa: a judged problemset, ICPC-style contests with live standings, Elo-style ratings, a sandboxed multi-language judge, and a multi-judge archive that indexes ~26,000 problems from Codeforces, AtCoder, UVa and CSES.
- Backend: Node.js (Express 5) + SQLite (built-in
node:sqlite, WAL mode) — no native modules. - Judge: worker pool + a Python sandbox supervisor (Linux rlimits / per-worker uids, Windows Job Objects).
- Frontend: dependency-free SPA (ES modules), CodeMirror editor, KaTeX math, light/dark themes, mobile layout.
| Area | What you get |
|---|---|
| Problemset | 40 original problems (statements with TeX, samples, full generated test data), tags, difficulty, acceptance, solved/attempted marks, filters |
| Judge | C, C++17/20, Java, Python 3, PyPy 3, JavaScript, Go, Rust — auto-detected per judge node. Verdicts AC / WA / TLE / MLE / RE / CE / OLE, per-test time & memory, compile logs, checkers tokens, tokens-ci, lines, exact, float:EPS |
| Submitting | In-browser editor with per-problem drafts, file upload, custom test run, live verdict updates, online IDE page |
| Contests | Scheduling, registration, countdowns, problems hidden until start and locked out of the problemset until the end, ICPC scoring with penalty, first-solve highlights, standings freeze, per-contest submissions |
| Ratings | Codeforces-style rating algorithm, rank titles and colours, rating history graph, rollback |
| Profiles | Rating graph, activity heatmap, verdict/language/tag breakdowns, solved list, settings, password change |
| Archive | ~26k problems from Codeforces (11.4k, with ratings & tags), AtCoder (9.6k, with estimated difficulty), UVa (5k), CSES (400); search, tag/difficulty/category filters, random pick, personal solved/to-do tracking, Codeforces progress sync, custom JSON import for any other judge |
| Admin | Problem editor with markdown preview, test manager (upload .in/.out pairs, samples, view, delete), generate expected outputs from a reference solution, rejudge, contest editor, rating apply/rollback, user roles & bans, archive sync, announcements, judge node dashboard |
| Security | scrypt passwords, DB-backed httpOnly sessions, CSRF header guard, CSP, rate limits, input validation, hidden-test checker messages, source visibility rules |
Requirements: Node.js ≥ 22.13, Python ≥ 3.8 (the sandbox supervisor), plus whichever compilers you
want to offer (g++, gcc, javac, pypy3, go, rustc are detected automatically; Python and Node work out of the box).
npm install
npm run seed # admin account + 40 problems with generated tests + demo contests (~10 s)
npm run import # optional: fetch the multi-judge archive (~26k problems, ~15 s)
npm start # http://localhost:3000The admin password generated by the seeder is in data/ADMIN_CREDENTIALS.txt — change it on the Settings page.
npm run demo optionally fills the finished warm-up contest with judged submissions from demo_* accounts so
standings, ratings and profiles have something to show (don't run it on a real site).
CodeArena needs a long-running server with a persistent disk: the SQLite database and ~220 MB of
generated test data live on disk, judge workers poll a queue continuously, and submissions are compiled
and executed with real toolchains inside a process sandbox. The Docker image does everything on first
boot (seeds the problem set, imports the archive) — you only set ADMIN_PASSWORD and attach a volume at /data.
| Host | Steps |
|---|---|
| Railway | New Project → Deploy from GitHub repo (uses railway.json + Dockerfile) → add a Volume mounted at /data → Variables: ADMIN_PASSWORD → Settings → Networking → Generate domain. |
| Fly.io | fly launch --copy-config --no-deploy → fly volumes create codearena_data --size 5 → fly secrets set ADMIN_PASSWORD=… → fly deploy (see fly.toml). Runs in a micro-VM, so submissions also get their own empty network namespace. |
| Render | New → Blueprint → this repo (render.yaml, paid instance for the disk) → enter ADMIN_PASSWORD. |
| Any VPS | docker compose up -d --build (below) — the most hardened option. |
The first boot takes a minute or two (problem generation + archive import); watch the logs for
listening. The Admin → Dashboard shows each judge node's isolation status (own uid / network).
Why not Vercel or Netlify? They run short-lived serverless functions with a read-only, ephemeral filesystem and no background processes. The database and test data would vanish between invocations, nothing could keep judging the queue, and there are no compilers or process sandbox — pages would load but logins, submissions and judging would not work.
cp .env.example .env # set ADMIN_PASSWORD, SITE_NAME; SECURE_COOKIE=false only for plain-HTTP testing
docker compose up -d --build
docker compose up -d --scale judge=3 # more judging capacitydocker-compose.yml runs two roles from one image:
- web — API + SPA as an unprivileged user. It never runs user code: custom test runs and answer
generation are queued to the judge (
adhoc_jobs). - judge — worker pool with no network, read-only root filesystem, all capabilities dropped except
what
setuidneeds,no-new-privileges, a PID limit, and every submission executed as its own unprivileged uid.
Put nginx/Caddy in front for TLS (TRUST_PROXY=true is the image default).
Browser (SPA) ──HTTP/JSON──► web: Express API ──► SQLite (WAL) ◄── judge worker pool(s)
▲ ▲ │ claim: UPDATE … RETURNING
archive importers ──┘ │ ▼
(Codeforces, AtCoder, │ sandbox.py --serve (one per worker)
UVa, CSES public APIs) │ ├─ compile (limits, own uid)
│ └─ run each test (CPU/mem/output/pids)
data/tests/<problem>/<n>.in|.out (owner-only)
- Submissions are a queue in the
submissionstable. Workers claim jobs with one atomicUPDATE … WHERE status='queued' … RETURNING, so any number of worker processes can share the DB. Crashed workers are detected (dead PID / stale lock) and their jobs requeued. - Judge nodes publish heartbeats (languages, load); the UI only offers languages a live node can judge.
- Judging stops at the first failed test (configurable). Verdicts refresh per-user/per-problem counters.
server/judge/sandbox.py is a long-lived supervisor (one per worker) that runs each program with:
| Linux (production) | Windows (development) | |
|---|---|---|
| CPU time | RLIMIT_CPU + live /proc polling (cut at TL), exact wait4 rusage |
Job Object accounting, polled |
| Memory | RLIMIT_AS (native) / max RSS check (JVM, V8, Go) |
Job Object hard commit limit + peak |
| Processes | RLIMIT_NPROC per uid, process-group kill, uid-wide sweep after every run |
Job active-process limit, kill-on-close |
| Output | pipe byte counter + RLIMIT_FSIZE |
pipe byte counter |
| Identity | SANDBOX_UID + worker (no files, no login), minimal env |
same user as the server |
| Filesystem | data dir 0700 (enforced on boot), work dir 0711, per-run dir 0700 |
not isolated |
| Network | fresh empty network namespace per run (needs root + CAP_SYS_ADMIN, e.g. Fly.io VMs); network_mode: none in Compose |
not isolated |
Verified against hostile submissions (reading /etc/shadow at compile or run time, reading test answers,
listing other runs, fork bombs, orphaned daemons holding stdout, memory bombs, output floods, busy loops,
sleeping, opening outbound sockets). Windows mode is for development only — a submission there runs as your user. For a public
deployment use the Linux judge container (optionally add seccomp/nsjail for defence in depth).
npm run import [source…] (or Admin → Archive → Sync now) pulls metadata only — title, tags,
difficulty, solve counts, link — from:
| Source | Endpoint |
|---|---|
| Codeforces | official API problemset.problems |
| AtCoder | AtCoder Problems community dataset (kenkoooo) — problems + difficulty model |
| UVa | uHunt API |
| CSES | the public problem list page |
Statements and test data belong to the original judges and are never copied; every archive entry links to the original statement. SPOJ blocks automated access (HTTP 403) and LightOJ has no public API, so they are not scraped; add their problems (or any other judge's) with a JSON list instead:
npm run import -- --file spoj.json --source spoj[{ "external_id": "TEST", "title": "Life, the Universe, and Everything",
"url": "https://www.spoj.com/problems/TEST/", "difficulty": 800, "tags": ["implementation"] }]Set ARCHIVE_AUTO_SYNC_HOURS=24 to refresh automatically.
Admin → Problems → New problem:
- Write the statement (markdown with
$inline$/$$display$$TeX), limits, checker, tags, difficulty. - Add tests: paste them, or drop files named
1.in+1.out(or.ans); mark samples to show them in the statement. - Optionally upload only inputs and generate expected outputs from a reference solution.
- Set visibility to public, or put the problem into a contest — it stays hidden from the problemset until the contest ends.
Built-in problems live in seed/problems/*.js (statement + deterministic generator + reference solver);
seed/solutions/ holds independent reference solutions, and npm run verify re-checks every test file
through the real judge.
All settings are environment variables (or a .env file) — see .env.example.
The most important ones:
| Variable | Default | Meaning |
|---|---|---|
PORT / HOST |
3000 / 0.0.0.0 |
HTTP listener |
DATA_DIR |
data |
SQLite database and test data |
ADMIN_PASSWORD |
– | creates the ADMIN_HANDLE admin if missing; changing the value resets that admin's password once on the next boot (recovery without shell access) |
JUDGE_EMBEDDED |
true |
run judge workers inside the web process |
JUDGE_WORKERS |
CPUs − 1 (max 4) | parallel judgements per process |
SANDBOX_UID / SANDBOX_GID |
– | Linux: base uid for submissions (supervisor must be root) |
SANDBOX_PYTHON |
python3 / python |
interpreter for the supervisor |
SUBMIT_COOLDOWN_MS |
5000 |
minimum gap between a user's submissions |
AUTO_SEED / AUTO_IMPORT_ARCHIVE |
off (on in Docker) | build problems / import the archive on first boot |
SECURE_COOKIE, TRUST_PROXY |
off | enable behind HTTPS / a reverse proxy |
ARCHIVE_AUTO_SYNC_HOURS |
0 |
periodic archive refresh |
| Command | Purpose |
|---|---|
npm start / npm run dev |
start the server (dev = auto-restart) |
npm run judge |
standalone judge worker pool (with JUDGE_EMBEDDED=false on the web node) |
npm run seed [codes…] |
create admin, build problems/tests, demo contests |
npm run import [sources…] |
sync the archive |
npm run verify [codes…] |
run reference solutions against all tests through the judge |
npm run smoke |
end-to-end test on a throw-away instance (41 checks: auth, contest, verdicts, standings, ratings, visibility); SMOKE_SPLIT=1 tests web + separate judge |
npm test |
unit tests (checkers, rating system) |
npm run demo |
optional demo contest data |
Everything the UI does goes through /api (see the About & API page). Mutating requests need a session
cookie and the header X-Requested-With: CodeArena.
GET /api/problems?q=&tags=dp,graphs&minDiff=&maxDiff=&status=&sort=&page=
GET /api/problems/:code[?contest=id]
POST /api/submissions { problem, language, source, contest? }
GET /api/submissions?user=&problem=&verdict=&language=&contest=&mine=1
GET /api/submissions/:id
POST /api/run { language, source, input }
GET /api/contests | /api/contests/:id | /api/contests/:id/standings
GET /api/users/:handle | /api/users/rankings?by=rating|solved
GET /api/archive?source=&q=&tags=&minDiff=&maxDiff=&status=&sort= | /api/archive/random
/api/admin/* problem, test, contest, user, archive and announcement management
server/
index.js, app.js, config.js, db.js, schema.sql
judge/ languages.js sandbox.py sandbox.js checker.js judge.js tasks.js worker.js
routes/ auth problems submissions contests users archive admin meta
services/ contests (standings, ratings) rating problems tests stats archive/
middleware/ auth.js util/ http, validate, security, ratelimit
public/ index.html css/app.css js/{app,api,ui,markdown,editor}.js js/pages/*.js
seed/ seed.js lib.js problems/set1-4.js solutions/*.py|*.cpp
scripts/ import-archive.js verify-solutions.js smoke-test.js demo-data.js
Dockerfile, docker-compose.yml, railway.json, fly.toml, render.yaml
test/ checker.test.js rating.test.js
- SQLite in WAL mode comfortably handles a busy community site on one host. For multi-host deployments,
move
server/db.jsto PostgreSQL and the rate limiter to Redis — the SQL is standard and the job claim maps toSELECT … FOR UPDATE SKIP LOCKED. - Natural extensions: custom checkers / interactive problems (the checker module is the extension point), virtual participation, clarifications, blogs/editorials, team contests, and seccomp/nsjail profiles.