Head-to-head Codeforces racing. Challenge a friend or quick-match by Elo, get the same problem at the same instant, and race to the first Accepted — face to face over voice and video.
Live at cph2h.vercel.app.
- Link your Codeforces handle. Ownership is proven with a compile-error challenge — you submit a deliberate
COMPILE_ERRORto an assigned problem and the platform confirms it via the public CF API. No password is ever collected. - Find an opponent. Send a challenge link to a friend, or join the quick-match queue and get paired by Elo.
- Ready up. Both players confirm, a countdown runs, and the problem is revealed to both at the same instant — never earlier.
- Race. Solve locally and submit on codeforces.com as usual. The platform polls the public CF API and picks up your verdict within seconds — no manual reporting.
- Win. First
Acceptedtakes the race. Elo updates exactly once, atomically. Leaving mid-race forfeits after a grace period.
- Challenge links & quick-match — direct invites, or Elo-based pairing with rating and contest-date problem filters
- Live voice & video — LiveKit-powered opponent spotlight while you race
- Automatic verdicts — detected straight from Codeforces via API polling
- Elo ladder — provisional and standard K-factors, public leaderboard
- Fair problems — server-side gated until the synchronized start; filters exclude problems either player has already solved
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router), React 19, deployed on Vercel |
| Database | Neon Postgres + Drizzle ORM (HTTP driver; atomic single-statement state transitions) |
| Auth | Clerk |
| Real-time | LiveKit Cloud — voice/video and race events (events are hints; the REST snapshot is the source of truth) |
| Codeforces | Public CF API only — verdict polling, handle verification, problem metadata |
Prerequisites: Node.js 20+, pnpm, and free-tier accounts for Neon, Clerk, and LiveKit Cloud.
git clone https://github.com/C0derTang/cph2h.git
cd cph2h
pnpm install
cp .env.example .env.local # fill in values — see docs/deployment.md
pnpm exec drizzle-kit migrate
pnpm exec tsx --env-file=.env.local scripts/populate-problems.ts # seed the CF problem pool
pnpm devOpen http://localhost:3000 and sign in with Clerk. Every environment variable and where to get it is documented in docs/deployment.md.
pnpm dev # dev server on :3000
pnpm exec eslint . # lint
pnpm exec tsc --noEmit # typecheck
pnpm exec vitest run # unit tests (tests/**/*.test.ts)
pnpm test:e2e # Playwright smoke test (needs a running app + e2e env vars)
pnpm exec drizzle-kit generate # create a migration from a schema change
pnpm exec drizzle-kit migrate # apply migrationsCI runs lint, typecheck, and unit tests. The Playwright smoke test is deliberately not a CI gate.
Pure logic lives in src/lib/; routes in src/app/api/ are thin shells that validate, call pure functions, and persist.
src/lib/race/— race lifecycle (pending → ready → active → finished|aborted), verdict polling, presence/forfeitsrc/lib/matchmaking.ts— quick-match pairing (single-statement claim withFOR UPDATE SKIP LOCKED)src/lib/cf/— Codeforces API client, handle verification, solve-history importsrc/lib/types.ts— the shared contract: race DTOs and the LiveKit event union
Invariants worth knowing before touching anything:
- Every state transition is an atomic compare-and-swap:
UPDATE ... WHERE status='<expected>' RETURNING *; zero rows means you lost the race — re-read. finishRaceis idempotent and is the sole place Elo is applied.- The problem statement is never served before
startedAt. - LiveKit events are hints; clients refetch
GET /api/races/[id]whenever an event can't be applied cleanly.
Deep dives: docs/architecture.md · docs/deployment.md · docs/design.md
Personal project — issues and PRs welcome, or fork and extend.