Skip to content
oiibarPublic

About

A full-stack speedcubing timer featuring WCA-style inspection, 2x2–7x7 scramble generation, accurate solve statistics, persistent solve history, and performance analytics. Built with React, TypeScript, NestJS, PostgreSQL, Docker, and Redux Toolkit.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

CS Timer

A focused speedcubing timer: WCA-style inspection, scrambles for every NxN cube from 2x2 to 7x7, correct averages and a synced solve history with progress charts.

CS Timer timer page with statistics and recent solves

Why

Speedcubers time hundreds of solves a week and judge progress by their averages (ao5, ao12, ao100). Those numbers are only useful when they follow the World Cube Association rules: +2 and DNF penalties, trimmed averages, DNFs ranking worst. Many simple timers get this wrong or keep times only in one browser. CS Timer records every solve to your account, computes the statistics on the server with the WCA rules, and shows whether your averages are actually dropping.

CS Timer is an independent portfolio project inspired by csTimer. It is not affiliated with csTimer or the WCA.

Live demo: not deployed. The repository contains a production-ready deployment setup (docs/deployment.md), but no public instance is running.

Features

  • Timer: hold-to-start with a 300 ms ready state (space bar or touch), optional 15-second WCA inspection with automatic +2 and DNF, hide time while solving, focus mode that hides the page chrome, Esc to cancel.
  • Scrambles: random-move scrambles for 2x2 to 7x7 with no redundant moves on the same axis (ADR 0006).
  • Statistics: single, mo3, ao5, ao12, ao50 and ao100, current and best, with a toast when you set a personal best.
  • History: paginated solve list per puzzle, details dialog, change the penalty, delete one or all, CSV export.
  • Progress: chart of the last 50 to 1000 solves with rolling ao5 and ao12.
  • Accounts: sign up, e-mail verification, password reset, change password, log out everywhere, delete account.
  • Reliability: offline-safe saves. Each solve carries a client ID, so a retry after a network error never creates a duplicate (ADR 0003).
  • Quality: responsive, keyboard accessible, axe-checked, dark and light themes.
History Solve details Progress
Solve history Solve details dialog Progress chart
Landing Mobile
Landing page Timer on mobile

Tech stack

Area Choice Why
Web React 19, Vite 7, Redux Toolkit with RTK Query, Tailwind CSS, Recharts Kept from the original project. RTK Query handles caching and refetching after each solve
API NestJS 11 (TypeScript), REST with OpenAPI Modules, DI and guards give clear boundaries in a modular monolith
Database PostgreSQL 17, TypeORM 0.3 with versioned migrations Constraints enforce valid puzzles, times and one row per client ID
Auth HttpOnly cookie sessions: 15-minute JWT plus rotating refresh tokens, Argon2id Scripts cannot read the session, and sessions are revocable (ADR 0002)
Edge nginx in the web image, Caddy 2 in production One origin for web and API, automatic HTTPS (ADR 0005)
Ops Docker, Compose, GitHub Actions, GHCR Reproducible builds, one-command local stack, gated releases
Observability pino JSON logs, Prometheus metrics, Terminus health checks, optional Sentry Diagnosable without a heavy monitoring stack
Tests Jest, Supertest, Vitest, Testing Library, Playwright, axe, k6 Unit to end-to-end coverage of the critical paths

Deliberately not used: microservices, Kubernetes, Redis or queues. At this scale they add operating cost without benefit (ADR 0001).

Architecture

flowchart LR
  Browser -- HTTPS --> Caddy
  Caddy -- "/api/*" --> API["NestJS API<br/>auth · users · solves · health"]
  Caddy -- "everything else" --> Web["nginx<br/>static React SPA"]
  API --> DB[(PostgreSQL)]
  API --> SMTP[SMTP]
Loading

More: architecture · system diagram · ER diagram · timer state machine · auth sequences · deployment · ADRs.

Getting started

You need Docker with Compose v2. Node.js 24 is only needed to run tests or develop outside containers.

git clone https://github.com/oiibar/CS_Timer.git
cd CS_Timer
cp .env.example .env
# set JWT_SECRET in .env, for example with:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"

docker compose up -d --build --wait   # db, mailpit, api, web
docker compose run --rm migrate       # create the schema
docker compose run --rm seed          # optional demo data
URL What
http://localhost:3100 Web app
http://localhost:4100/api/docs Swagger UI (OpenAPI)
http://localhost:8026 Mailpit, where verification and reset e-mails land

The demo login after seeding is demo@cstimer.dev with password Demo-password-1. It holds 630 solves across three puzzles. The ports avoid the usual 3000, 4000 and 5432, so the stack runs next to other projects.

Stop with docker compose down. Add -v to also delete the database volume.

Developing with hot reload

docker compose up -d db mailpit
cd server && cp .env.example .env && npm ci     # set JWT_SECRET in server/.env
npm run migration:run && npm run seed && npm run start:dev
cd ../client && npm ci && npm run dev           # proxies /api to localhost:4100

Environment variables

Compose reads the root .env.example. The API's full list, with defaults, is in server/.env.example. It is validated at startup, and the API refuses to boot and names the problem if a value is missing or invalid. The web client has no runtime configuration; it always calls the API on its own origin under /api.

Variable Required Purpose
JWT_SECRET yes Signs access tokens, 32 or more characters
DATABASE_URL yes, set by Compose PostgreSQL connection
DATABASE_SSL, DATABASE_CA_CERT for hosted Postgres TLS to a managed database
APP_URL, CORS_ORIGINS defaults to localhost Public origin for links, CSRF and CORS checks
MAIL_TRANSPORT, SMTP_*, MAIL_FROM for e-mail smtp (Mailpit locally) or log
COOKIE_SECURE, TRUST_PROXY production Secure cookies, trust the proxy's client IP
SENTRY_DSN no Error tracking

Database

Migrations live in server/src/database/migrations and run as an explicit step. The seed is deterministic and uses fictional accounts only.

./scripts/backup-db.sh
./scripts/restore-db.sh backups/<file>.dump            # rehearse into a scratch DB
./scripts/restore-db.sh backups/<file>.dump --replace  # real restore

Details are in docs/database.md and docs/disaster-recovery.md.

Tests and quality checks

# server/  (API tests need the Compose db and a cstimer_test database, see docs/testing.md)
npm run lint && npm run typecheck && npm test && npm run test:e2e

# client/
npm run lint && npm run typecheck && npm test
npm run test:e2e        # Playwright against the running stack

Latest local run on 2026-11-10, all passing:

Suite Tests
Server unit 48
Server API and integration, against real PostgreSQL 54
Client unit and component 58
Playwright, desktop and mobile, with axe 11

Combined server coverage is 94% of lines and 70% of branches. Under a k6 load test ramping to 50 users, the API served 118 requests per second with no errors, and every endpoint stayed under 55 ms at p95. That run is on a laptop with seed data. Conditions, and the statistics optimisation that cut the stats p95 from 658 ms to 40 ms, are in docs/performance.md.

CI on GitHub Actions runs format, lint, typecheck, all test suites, builds, npm audit, Trivy image scans, a Gitleaks secret scan and Playwright against the Compose stack on every push and pull request.

Deployment

The target is one Linux VM with Docker Compose and Caddy, which handles Let's Encrypt automatically. Tagging vX.Y.Z builds and publishes images to GHCR. After approval on the production environment, CI deploys over SSH: backup, migrate, roll out, smoke test. Rollback means redeploying the previous tag. The full guide is docs/deployment.md, and operations are in docs/operations-runbook.md.

Security

  • Passwords and sessions: Argon2id hashes, HttpOnly SameSite cookies, rotating refresh tokens with reuse detection.
  • Authorization: every solve query is scoped to its owner, and other users' solves return 404.
  • Input and abuse: strict DTO whitelisting, per-IP rate limits with a stricter limit on credential endpoints, CSRF origin checks.
  • Headers and output: CSP and security headers, CSV export guarded against formula injection, credentials and tokens redacted from logs.

See docs/security.md and SECURITY.md.

Known limitations and next steps

  • Not deployed publicly yet. Production backups and monitoring are documented but not exercised on a real host.
  • Scrambles are random-move, not random-state. Competition-grade 3x3 scrambles need a solver such as min2phase (ADR 0006).
  • Only NxN cubes. No Pyraminx, Megaminx, Square-1, blindfolded or one-handed events, and no session splitting within a puzzle.
  • Solves need a connection. A failed save can be retried, but there is no offline queue across page reloads.
  • Rate limits are per process. Several replicas would need Redis-backed throttling.
  • No licence has been chosen yet, so all rights are reserved by the author.

Documentation

Product requirements · Architecture · API · Database · Security · Testing · Performance · Deployment · Runbook · Disaster recovery · Audit of the original code · Contributing · Changelog

Author

Built by oiibar. Contact: contact-me-neon.vercel.app.

About

A full-stack speedcubing timer featuring WCA-style inspection, 2x2–7x7 scramble generation, accurate solve statistics, persistent solve history, and performance analytics. Built with React, TypeScript, NestJS, PostgreSQL, Docker, and Redux Toolkit.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages