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.
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.
- 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 |
|---|---|---|
![]() |
![]() |
![]() |
| Landing | Mobile |
|---|---|
![]() |
![]() |
| 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).
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]
More: architecture · system diagram · ER diagram · timer state machine · auth sequences · deployment · ADRs.
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.
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:4100Compose 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 |
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 restoreDetails are in docs/database.md and docs/disaster-recovery.md.
# 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 stackLatest 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.
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.
- 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.
- 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.
Product requirements · Architecture · API · Database · Security · Testing · Performance · Deployment · Runbook · Disaster recovery · Audit of the original code · Contributing · Changelog
Built by oiibar. Contact: contact-me-neon.vercel.app.





