An after-hours training journal. Log sets, track body weight and steps, detect PRs automatically, and watch your volume history render as a live heatmap.
- Horizontal exercise columns with drag-to-reorder (dnd-kit)
- Drag-gesture numeric inputs — pull up/down on weight, left/right on reps
- Automatic PR detection per set using the Epley e1RM formula
- PR sets glow with an animated chrome-to-acid conic gradient
- Rest timer with 60/90/120/180s presets and an AudioContext ping at zero
- Press
Ato open the exercise picker without touching the mouse
- Image upload with drag-and-drop, browser-side AVIF re-encode before upload
- Images stored on Backblaze B2 via signed PUT URLs (5-min TTL)
- YouTube reference link per exercise (opens technique video inline)
- Live search
- Weekly volume with % delta vs prior 7 days
- Session count, set count, weekly PRs, training streak
- 12-month volume heatmap rendered with Three.js InstancedMesh (371 tiles, sqrt-normalized brightness)
- Stalled lifts — exercises with no e1RM improvement in 4+ weeks
- Latest body weight + 7-day step average
- Scrubbable SVG line chart — acid raw line, violet 7-day rolling average, floating tooltip
- Drag-gesture quick-log input
- Full history with delete
⌘Kcommand palette- G-chord keyboard navigation:
G E→ Exercises,G S→ Sessions,G D→ Dashboard,G W→ Weight,G T→ Steps - Acid-green stripe sweeps the top of the screen on every route change (View Transitions API)
- Installs as a PWA — offline shell cached via service worker
- Argon2id password hashing (OWASP 2024 parameters)
- Iron-session sealed cookie (30-day TTL, httpOnly, SameSite=strict)
- CSRF double-submit pattern (csrf-csrf)
- Helmet with strict Content-Security-Policy
- CORS restricted to
WEB_ORIGINonly - All queries scoped to the authenticated owner — no cross-user data access possible
- Rate limiting on auth endpoints (5/min register, 10/min login)
| Layer | Choice |
|---|---|
| Monorepo | Turborepo + pnpm workspaces |
| Frontend | Next.js 16 (App Router, Turbopack) |
| Styling | Tailwind v4 CSS-first @theme |
| Animations | Motion/React |
| 3D | Three.js (InstancedMesh heatmap) |
| Drag & drop | dnd-kit |
| Server state | TanStack Query v5 |
| Backend | NestJS 11 |
| Database | PostgreSQL via Prisma 7 + pgBouncer |
| Auth | iron-session + Argon2id |
| Storage | Backblaze B2 (S3-compatible) |
| Validation | Zod (shared contracts package) |
| Logging | nestjs-pino |
gymtrace/
├── apps/
│ ├── api/ # NestJS backend (port 3001)
│ └── web/ # Next.js frontend (port 3000)
└── packages/
├── contracts/ # Shared Zod schemas + DTOs (built to dist/)
├── ui/ # Shared React component library
└── config-tailwind/ # Night Garage design tokens + base CSS
- Node.js 20+
- pnpm 9+
- A PostgreSQL database (e.g. Supabase free tier)
- A Backblaze B2 bucket with S3-compatible API enabled
pnpm installCopy the example env file and fill in the values:
cp apps/api/.env.example apps/api/.env| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string (pooled, for runtime) |
DIRECT_URL |
PostgreSQL connection string (direct, for migrations) |
SESSION_SECRET |
≥32 random chars — openssl rand -hex 32 |
CSRF_SECRET |
≥32 random chars — openssl rand -hex 32 |
B2_KEY_ID |
Backblaze B2 application key ID |
B2_APPLICATION_KEY |
Backblaze B2 application key |
B2_BUCKET_NAME |
Bucket name |
B2_ENDPOINT |
S3 endpoint URL e.g. https://s3.eu-central-003.backblazeb2.com |
B2_PUBLIC_BASE_URL |
Public CDN URL e.g. https://f003.backblazeb2.com/file/bucket-name |
WEB_ORIGIN |
Frontend origin for CORS e.g. http://localhost:3000 |
pnpm --filter api prisma:migrate:devpnpm devThis starts both the API (:3001) and the web app (:3000) in parallel.
Open http://localhost:3000 and create an account.
All routes require authentication except /auth/login, /auth/register, /auth/csrf, and /health/*.
GET /health/live
GET /health/ready
GET /api/auth/csrf
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
GET /api/exercises?q=
POST /api/exercises
PATCH /api/exercises/:id
DELETE /api/exercises/:id
GET /api/sessions
POST /api/sessions
GET /api/sessions/:id
DELETE /api/sessions/:id
POST /api/sessions/:id/exercises
PATCH /api/sessions/:id/exercises/order
DELETE /api/sessions/:id/exercises/:seId
POST /api/sessions/:id/exercises/:seId/sets
PATCH /api/sessions/:id/exercises/:seId/sets/:setId
DELETE /api/sessions/:id/exercises/:seId/sets/:setId
GET /api/body-weight?days=
POST /api/body-weight
DELETE /api/body-weight/:id
GET /api/steps?days=
POST /api/steps
DELETE /api/steps/:id
GET /api/stats/dashboard
GET /api/media/upload-url?filename=&contentType=
pnpm dev # start all apps in watch mode
pnpm build # build all apps and packages
pnpm check-types # type-check entire monorepo
pnpm --filter api prisma:generate # regenerate Prisma client
pnpm --filter api prisma:migrate:dev # run migrations (development)
pnpm --filter api prisma:migrate:deploy # run migrations (production)