A coach-facing admin and client-facing training tracker built with Payload CMS and Next.js. Coaches build workout plans in the admin panel; clients log their sets through a mobile-friendly web interface.
Coach (admin panel) — creates plans, assigns them to clients, defines workout structure down to individual exercise rows with target sets and tracking parameters.
Client (web app) — logs in, sees their active plan, works through workouts session by session, and logs each set (reps, weight, RIR, time, etc.).
The data splits into two layers. The plan layer is the template a coach authors (read-only for clients). The log layer is what a client records while training. The two never mix: logging never writes to the plan, so the same template can be reused and audited.
PLAN LAYER (authored by coach) LOG LAYER (recorded by client)
───────────────────────────── ──────────────────────────────
Plan WorkoutLog ............ one training session
└─ Microcycle ├─ SetLog ............. one logged set (N per exercise)
└─ Workout ├─ ExerciseLog ........ one note per exercise (1 per exercise)
├─ sections[] "Warm-up", "Main" └─ RoundLog ........... one round of a group (reserved)
└─ WorkoutGroup → in a section
└─ WorkoutExerciseRow Catalog: Exercise, Media
└─ Exercise (catalog, opt.) Accounts: User (coach), Client (athlete)
Sharing: ShareLink
sections[]is not a collection — it's an array of named headings (title/subtitle) stored on theWorkout. AWorkoutGroupattaches to one section via itssectionRowId. So the visible nesting in the app is Workout → Section → Group → Exercise row, while in the database a group points back to its section by id.
A WorkoutLog references the Workout it belongs to and the Client who owns it. Each SetLog / ExerciseLog references its session (the WorkoutLog) and the exerciseRow (WorkoutExerciseRow) it logs against — that exerciseRow link is the key that ties execution back to the exact line in the plan.
erDiagram
CLIENT ||--o{ PLAN : owns
PLAN ||--o{ MICROCYCLE : has
MICROCYCLE ||--o{ WORKOUT : has
WORKOUT ||--o{ WORKOUT_GROUP : has
WORKOUT_GROUP ||--o{ WORKOUT_EXERCISE_ROW : has
EXERCISE ||--o{ WORKOUT_EXERCISE_ROW : "referenced by"
CLIENT ||--o{ WORKOUT_LOG : logs
WORKOUT ||--o{ WORKOUT_LOG : "session of"
WORKOUT_LOG ||--o{ SET_LOG : has
WORKOUT_LOG ||--o{ EXERCISE_LOG : has
WORKOUT_LOG ||--o{ ROUND_LOG : has
WORKOUT_EXERCISE_ROW ||--o{ SET_LOG : "logged as (N per session)"
WORKOUT_EXERCISE_ROW ||--o{ EXERCISE_LOG : "noted as (1 per session)"
WORKOUT_GROUP ||--o{ ROUND_LOG : "round of"
PLAN ||--o{ SHARE_LINK : "shared via"
PLAN {
relationship client
select status
}
WORKOUT {
relationship microcycle
array sections "named headings — Group.sectionRowId points here"
}
WORKOUT_GROUP {
relationship workout
text sectionRowId "which section it belongs to"
select protocol "standard / emom / amrap / for_time / tabata"
}
WORKOUT_EXERCISE_ROW {
relationship group
relationship exercise "catalog link (optional)"
text targets "reps, kg, rir, tut, rest…"
}
WORKOUT_LOG {
relationship client
relationship workout
}
SET_LOG {
relationship session
relationship exerciseRow
number setNumber
}
EXERCISE_LOG {
relationship session
relationship exerciseRow
textarea note
}
ROUND_LOG {
relationship session
relationship group
number roundNumber "reserved — not yet written"
}
SHARE_LINK {
relationship plan
select permissions "plan / results"
date expiresAt
}
| Collection | Auth | Purpose |
|---|---|---|
users |
✅ | Coaches / staff. The only accounts allowed into /admin. |
clients |
✅ | Athletes. Log in to the client web app (never the admin). Holds name, a join to their plans, and admin-only notes (trainer notes, hidden from the client). 2h sessions, lockout after 5 failed logins. |
| Collection | Purpose |
|---|---|
exercises |
Reusable exercise catalog (name, muscle group, equipment, video, description). trackingType decides which metric fields the client sees in the logging form (e.g. weight+reps vs distance+time). Readable by any authenticated user; only coaches edit. |
media |
Image uploads (public read). |
| Collection | Belongs to | Purpose |
|---|---|---|
plans |
a client |
Top-level program. Status (active/paused/completed), date range, title, description. Versioned (audit trail). Client can read only their own. |
microcycles |
plan |
A block/week within the plan. Target rpe, order. |
workouts |
microcycle |
A single training day. Has an order, optional rpe, and sections[] (named blocks like "Warm-up", "Main part"). Edited via a custom Structure admin tab. Cannot be deleted once it has logged sessions. |
workout-groups |
workout |
A group of exercises sharing a protocol (Standard / EMOM / AMRAP / For Time / Tabata) and its parameters (rounds, durations, rest). Links to a workout section via sectionRowId. E.g. an "A1/A2 superset". Cannot be deleted if its rows have logged sets. |
workout-exercise-rows |
workout-group |
One prescribed exercise line. Optional link to a catalog exercise, plus targets (reps, kg, tut, rir, rest, duration), a plan note, optional per-set setParameters[] (drop sets/pyramids), and an override of the group protocol. Cannot be deleted if it has logged sets. |
| Collection | Keyed by | Cardinality | Purpose |
|---|---|---|---|
workout-logs |
client + workout |
one per session | A training session. Auto-titled, holds startedAt / finishedAt and general session notes. Creating the first set auto-creates the session. |
set-logs |
session + exerciseRow (+ setNumber) |
N per exercise | One logged set: weight, reps, RIR, distance, duration, bodyweight flag, per-set note. A beforeValidate hook strips any metric not allowed by the exercise's trackingType. |
exercise-logs |
session + exerciseRow |
1 per exercise | A single client note for the whole exercise in that session (vs. set-logs, which is per set). Same relations as set-logs so it can grow beyond a note later. Upserted from the tracker. |
round-logs |
session + group |
one per round | Per-round execution of a group (round number, status, timing). Reserved — defined in the schema but not yet written by the app. |
For every log collection: a client may only create/read/update/delete their own rows (adminOrOwnByClient), and the owning client is always set server-side from the session — never trusted from the request.
| Collection | Purpose |
|---|---|
share-links |
A tokenized, read-only link to a plan. permissions choose what is exposed (plan preview and/or results logs); expiresAt + active gate it. Only coaches manage links; the public access happens via the share-token cookie, which canReadViaShareToken validates to scope reads to that plan owner's data. |
- Author — coach creates a
Client, then aPlan→Microcycle→Workout, and builds structure (WorkoutGroup→WorkoutExerciseRow) on the workout's Structure tab, linking each row to a catalogExercise. - Assign — the plan is owned by the client; they log in and see only their own active plan.
- Train — opening a workout creates a
WorkoutLogon first save. The client logs each set as aSetLog(fields driven by the exercise'strackingType) and can attach oneExerciseLognote per exercise. - Review — coach reads the client's logs in the admin; deleting plan structure that already has logs is blocked to protect history.
- Share (optional) — a
ShareLinkexposes a read-only plan and/or results to anyone with the link until it expires.
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| CMS / Auth | Payload CMS 3 |
| Database | PostgreSQL (@payloadcms/db-postgres) |
| Styling | Tailwind CSS |
| i18n | next-intl (Polish / English) |
| Forms | react-hook-form |
| Icons | lucide-react |
- Node.js ≥ 24
- Yarn 4 (
corepack enable) - PostgreSQL database
git clone <repo-url>
cd training-app
yarn install
cp .env.example .envEdit .env:
DATABASE_URL=postgresql://user:password@localhost:5432/training_app
PAYLOAD_SECRET=your-long-random-secret-hereRun migrations:
yarn payload migrateyarn dev- App:
http://localhost:3000/pl - Admin panel:
http://localhost:3000/admin
- Open
/adminand create the first user account — this becomes the super-admin - (Optional) seed demo data:
yarn seed - Create a Client record for each athlete
- Build a Plan, link microcycles → workouts → exercise rows
- The client logs in at
/plusing their email and password set in the admin
src/
├── access/ # Shared access control functions (isAdmin, isAuthenticated…)
├── app/
│ ├── [locale]/(frontend)/ # Client-facing app (login, workout tracker)
│ └── (payload)/ # Payload admin routes and API
├── collections/ # Payload collection configs (one folder per collection)
│ ├── clients/
│ ├── exercises/
│ ├── plans/
│ ├── microcycles/
│ ├── workouts/
│ ├── workout-groups/
│ ├── workout-exercise-rows/
│ ├── workout-logs/
│ ├── set-logs/
│ ├── exercise-logs/
│ ├── round-logs/
│ ├── share-links/
│ ├── media/
│ └── users/
├── components/
│ ├── common/ # App-wide UI (logout button…)
│ ├── ui/ # Primitive components (button, input, surface…)
│ └── workout/ # Workout tracker components
├── data/ # Static/seed data
├── i18n/ # next-intl routing and request config
├── lib/ # Shared utilities and SDK client
├── loaders/ # Server-side data fetching functions
├── migrations/ # Payload database migrations
├── scripts/ # One-off CLI scripts (seed, import-plan…)
├── types/ # Shared TypeScript types
├── middleware.ts
├── payload-types.ts # Auto-generated — do not edit manually
└── payload.config.ts
.claude/skills/ # AI skills for Claude Code
.agents/skills/ # AI skills for Codex
.ai/specs/ # Feature specifications
| Script | Description |
|---|---|
yarn dev |
Start dev server |
yarn build |
Production build |
yarn start |
Start production server |
yarn payload migrate |
Run pending database migrations |
yarn generate:types |
Regenerate payload-types.ts from collection configs |
yarn generate:importmap |
Regenerate Payload admin import map (run after adding custom views) |
yarn seed |
Seed database with demo data |
yarn seed:export |
Export current database state to seed file |
yarn lint |
Run ESLint |
npx skills add <source> |
Install AI skills into .claude/skills/ and .agents/skills/ |
Follow .ai/skills/payload-build-collections — each collection lives in src/collections/{kebab-case}/index.ts and is registered in src/collections/index.ts.
After changing collection configs, regenerate types:
yarn generate:typesFollow .ai/skills/payload-build-modules. After registering a new component path, run:
yarn generate:importmapThis project uses skill files for AI-assisted development. Skills are managed with npx skills and installed into .claude/skills/ (Claude Code) and .agents/skills/ (Codex).
To install skills from the source repository:
npx skills add <source-path-or-url> -a claude-code -a codex --copySkills cover: Payload patterns, collection scaffolding, admin module structure, UI copy, and spec writing.
