https://github.com/codee-sh/payload-training-app
Open-source coaching app built with Payload CMS and Next.js. Coaches manage training plans in the admin; clients log workouts on mobile.
https://github.com/codee-sh/payload-training-app
coaching-app fitness-app nextjs opensource payload payloadcms postgres postgresql training-plan typescript workout-tracker
Last synced: 1 day ago
JSON representation
Open-source coaching app built with Payload CMS and Next.js. Coaches manage training plans in the admin; clients log workouts on mobile.
- Host: GitHub
- URL: https://github.com/codee-sh/payload-training-app
- Owner: codee-sh
- License: mit
- Created: 2026-06-14T13:31:39.000Z (12 days ago)
- Default Branch: main
- Last Pushed: 2026-06-22T12:43:46.000Z (4 days ago)
- Last Synced: 2026-06-22T13:25:42.115Z (4 days ago)
- Topics: coaching-app, fitness-app, nextjs, opensource, payload, payloadcms, postgres, postgresql, training-plan, typescript, workout-tracker
- Language: TypeScript
- Homepage:
- Size: 2.17 MB
- Stars: 9
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# Training App
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.
## What it does
**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.).
## Navigation
- [Data model & flow](#data-model--flow)
- [Tech stack](#tech-stack)
- [Getting started](#getting-started)
- [Project structure](#project-structure)
- [Key scripts](#key-scripts)
- [Development](#development)
- [Screenshots](#screenshots)
## Data model & flow
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 the `Workout`. A `WorkoutGroup` attaches to one section via its `sectionRowId`. 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.
### How a log row points back to the plan
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.
```mermaid
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
}
```
### Collections
#### Accounts
| 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. |
#### Catalog
| 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). |
#### Training plan (template — coach writes, client reads)
| 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. |
#### Training log (execution — client writes)
| 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.
#### Sharing
| 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. |
### End-to-end flow
1. **Author** — coach creates a `Client`, then a `Plan` → `Microcycle` → `Workout`, and builds structure (`WorkoutGroup` → `WorkoutExerciseRow`) on the workout's **Structure** tab, linking each row to a catalog `Exercise`.
2. **Assign** — the plan is owned by the client; they log in and see only their own active plan.
3. **Train** — opening a workout creates a `WorkoutLog` on first save. The client logs each set as a `SetLog` (fields driven by the exercise's `trackingType`) and can attach one `ExerciseLog` note per exercise.
4. **Review** — coach reads the client's logs in the admin; deleting plan structure that already has logs is blocked to protect history.
5. **Share** (optional) — a `ShareLink` exposes a read-only plan and/or results to anyone with the link until it expires.
## Tech stack
| 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 |
## Getting started
### Requirements
- Node.js ≥ 24
- Yarn 4 (`corepack enable`)
- PostgreSQL database
### Setup
```bash
git clone
cd training-app
yarn install
cp .env.example .env
```
Edit `.env`:
```env
DATABASE_URL=postgresql://user:password@localhost:5432/training_app
PAYLOAD_SECRET=your-long-random-secret-here
```
Run migrations:
```bash
yarn payload migrate
```
### Run
```bash
yarn dev
```
- App: `http://localhost:3000/pl`
- Admin panel: `http://localhost:3000/admin`
### First-time setup
1. Open `/admin` and create the first user account — this becomes the super-admin
2. (Optional) seed demo data: `yarn seed`
3. Create a **Client** record for each athlete
4. Build a **Plan**, link microcycles → workouts → exercise rows
5. The client logs in at `/pl` using their email and password set in the admin
## Project structure
```
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
```
## Key scripts
| 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 ` | Install AI skills into `.claude/skills/` and `.agents/skills/` |
## Development
### Adding a collection
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:
```bash
yarn generate:types
```
### Adding an admin view or custom field UI
Follow `.ai/skills/payload-build-modules`. After registering a new component path, run:
```bash
yarn generate:importmap
```
### AI skills
This project uses skill files for AI-assisted development. Skills are managed with [npx skills](https://github.com/vercel-labs/skills) and installed into `.claude/skills/` (Claude Code) and `.agents/skills/` (Codex).
To install skills from the source repository:
```bash
npx skills add -a claude-code -a codex --copy
```
Skills cover: Payload patterns, collection scaffolding, admin module structure, UI copy, and spec writing.
## Screenshots