§ Forge — hypertrophy tracker
Architecture — stack & data model
Forge is a small, sharply-separated Node app. The rule that keeps it maintainable: the database only shapes rows, the engine does all the math, and the renderers (art/charts) only draw. Each layer is independently testable.
The stack
- Express + EJS — server-rendered pages; a thin JSON API under
/apifor the interactive bits (set logging, rest timer, program edits). - Postgres 16 (in Docker) — the only persistent store.
- Vanilla client JS (
public/app.js) — no framework; handles set logging, the rest timer, PR stamps, and program editing viafetch. - Docker Compose —
db+app, app bound to127.0.0.1:3600so nginx is the only way in (the box's UFW/Docker rule). - PWA —
manifest.webmanifest+ a cache-firstsw.jsfor the static shell (never caches/api).
File map
| File | Responsibility |
|---|---|
catalog.js |
Domain data: 16 muscle groups, a 22-exercise catalog (each with muscle weightings, rep range, increment), and the seed PPL+UL program. Pure data. |
engine.js |
The math: double progression, Epley 1RM, the thermal fatigue model, weekly volume. Pure functions, no DB, no clock reads (caller passes now). |
db.js |
Schema, idempotent seeding from catalog.js, and every query — shaped exactly for the engine. |
art.js |
The anatomical SVG body map + exercise muscle diagrams + the plate-math glyph. |
charts.js |
Dependency-free inline SVG line/bar charts in the Forge palette. |
server.js |
Express routes; assembles the "today" model (engine targets per exercise) and the JSON API. |
views/*.ejs |
Today, Program, Body, Progress, Exercise, 404. |
engine.test.js |
Node test-runner unit tests for the pure math. |
e2e.mjs |
Exhaustive Playwright pass against the live URL. |
The data model
Five tables plus two catalog tables. The important design choice: fatigue and
progression are derived from set_logs, never stored. set_logs is the
single source of truth; everything else is computed on read.
muscles— key, name, body-map side (front/back/both).exercises— key, equipment, increment, rep range,musclesJSONB ({muscleKey: contribution}where 1.0 = prime mover, ~0.3–0.8 = assist), cues.program_days+program_exercises— the editable split.sessions— one per workout (day, started/finished, status).set_logs— the core record: session, exercise, set #, weight, reps, RPE, warmup flag, timestamp.
Idempotent seeding
On every boot, db.init() upserts muscles and exercises from catalog.js
(so catalog edits — a new cue, a tweaked muscle weighting — propagate), but seeds
the program only if it's empty (so your edits to the split survive restarts).
This "code is the catalog, DB is your data" split means you can evolve the
exercise library without migrations and without clobbering user changes.
The two interesting computations get their own pages: the progression engine and the fatigue model. The shared visual that powers both the heatmap and every exercise image is the body map.
Subpages