Data Model
Every Mongoose model, its fields, and how they relate. See Architecture for how these are used across a request.
Relationships at a glance
User ──┬── owns ──> WorkoutPlan (createdBy is a username, not a ref)
├── has one ──> WorkoutProgress (unique per user — one in-progress workout at a time)
├── has many ──> WorkoutSession (completed workout history)
├── has many ──> ExerciseHistory (last known reps/weight/duration, bucketed by rep count)
├── has many ──> ExerciseLog (one row per completed set, for charting)
└── has many ──> Subscription ──> WorkoutPlan (at most one active at a time)
WorkoutPlan ── days[] ── exercises[] ──> Exercise (by ref, or grouped into a superset)
License — one singleton document for the whole instance (not per-user)
Ownership fields (createdBy, username) are plain strings (lowercase usernames), not Mongoose
refs to User — the app doesn't enforce referential integrity at the database level for these;
lookups join by username string.
Exercise (models/exercise.js)
The exercise library — built-in and user-submitted.
| Field | Type | Notes |
|---|---|---|
name | String, required, trimmed | |
description | String | |
primaryMuscles / secondaryMuscles | [String] | Values from the fixed MUSCLE_GROUPS list (config/constants.js) |
equipment | [String] | Values from the fixed EQUIPMENT list |
instructions | [String] | Ordered steps |
type | enum reps | weight | duration, default reps | What kind of value gets logged when a set is completed |
image | String | Muscle-group diagram filename |
exerciseImage | String | Exercise-specific photo filename |
media | [originalName] | Uploaded photo/video attachments |
isBuiltIn | Boolean | Seeded exercises vs. user-submitted |
createdBy | String | Username |
No explicit indexes beyond _id. Referenced by WorkoutPlan.days[].exercises[].exercise,
ExerciseHistory.exercise, and ExerciseLog.exercise.
ExerciseHistory (models/exerciseHistory.js)
A "last known value" cache — one document per (username, exercise, reps) combination, so the same
exercise at different rep counts (e.g. a 5-rep set vs. a 12-rep set) remembers its own last
weight/duration independently.
| Field | Notes |
|---|---|
username, exercise (ref Exercise), reps | Compound bucket key |
lastReps, lastWeight, lastDuration | Prefilled into the workout-execution UI |
updatedAt |
Unique compound index: { username: 1, exercise: 1, reps: 1 } — enforced at the database level, so
there can never be two "last value" rows for the same person/exercise/rep-count bucket.
ExerciseLog (models/exerciseLog.js)
An append-only record of every completed set, used to build the trend charts on an exercise's detail
page. Unlike ExerciseHistory (overwritten in place), this grows forever.
| Field | Notes |
|---|---|
username, exercise (ref Exercise), reps, weight, duration | |
side | enum left | right, when the exercise tracks sides independently |
date | Defaults to now |
session | ref WorkoutSession — which completed workout this set belongs to |
Index: { username: 1, exercise: 1, date: 1 } (non-unique — supports the chronological chart query).
License (models/license.js)
A singleton document (_id: 'singleton') — one per instance, not per user. See
Licensing System for the full lifecycle.
| Field | Notes |
|---|---|
instanceId | UUID, generated once on first access, identifies this install to the licensing service |
key | The license key, or an offline token (AWPOFFLINE.… prefix) |
type | enum trial | lifetime | admin | null |
status | enum active | expired | revoked | unknown | null |
email, expiresAt | |
lastValidatedAt, lastValidationReachable | Bookkeeping for the revalidation scheduler — reachability failures don't change status |
Subscription (models/subscription.js)
The join between a user and a plan they've subscribed to.
| Field | Notes |
|---|---|
username (lowercase), plan (ref WorkoutPlan, required) | |
lastCompleted | Date the subscriber last finished this plan |
isActive | Whether this is the user's current "active plan" |
Indexes:
- Unique
{ username: 1, plan: 1 }— can't subscribe to the same plan twice. - Unique partial index
{ username: 1, isActive: 1 }whereisActive: true— enforces at most one active plan per user at the database level, not just in application code.
User (models/user.js)
| Field | Notes |
|---|---|
username | required, unique, lowercase, trimmed |
displayName | |
isAdmin | Boolean |
passwordHash | select: false by default (must be explicitly requested); never set for LDAP users |
mustChangePassword | Drives middleware/forcePasswordChange.js |
lastLogin | |
themeColors | { light: {...}, dark: {...} }, each with nullable text, textSecondary, bg, accent, menuText, menuBg — null means "use the app default" |
WorkoutPlan (models/workoutPlan.js)
The richest schema — a plan is nested days of nested exercises, with supersets as a further nested list inside an exercise slot.
supersetExerciseSchema (subdocument, no own _id): exercise (ref, required), reps,
duration, notes, trackSides.
planExerciseSchema (subdocument, no own _id) — one slot in a day, either a regular exercise or
a superset:
- Regular-exercise fields:
exercise(ref, required unless it's a superset),reps,duration,notes. - Shared fields:
sets(default 3),restBetweenSets(default 60s),restAfterExercise(default 60s),order,trackSides. - Superset fields:
isSuperset(Boolean),label,supersetExercises(array ofsupersetExerciseSchema).
daySchema (subdocument, no own _id): label (free text, not a calendar date), exercises
(array of planExerciseSchema).
Top-level WorkoutPlan: name (required), description, difficulty (enum
beginner/intermediate/advanced), category (enum
strength/cardio/hiit/flexibility/sports-specific/hybrid), estimatedDuration, tags
([String]), days ([daySchema]), isPublic (default true), createdBy (String, required),
subscriberCount (Number, default 0, denormalized count for the plan browser).
WorkoutProgress (models/workoutProgress.js)
An in-progress (paused or currently running) workout execution — distinct from a completed
WorkoutSession.
| Field | Notes |
|---|---|
username | required, unique — at most one in-progress workout per user, across every plan |
plan (ref, required), planName | Denormalized name in case the plan is later renamed/deleted |
dayIdx, dayLabel | Which day of the plan is being executed |
exerciseState | Mixed — mirrors the client-side execution state object verbatim (which sets are checked off, current exercise index, timers) |
exIdx, elapsedSeconds, startedAt, lastSavedAt |
Visible to admins under Admin → Sessions for clearing out entries stuck from a crashed tab.
WorkoutSession (models/workoutSession.js)
A completed workout, used for history, stats, and the profile activity heatmap.
| Field | Notes |
|---|---|
username (required) | |
plan (ref, optional) | Optional because a CSV-imported session may not match an existing plan by name |
planName (required) | Denormalized so history survives the source plan being deleted |
dayLabel, completedAt (default now), durationSeconds, exerciseCount |
Index: { username: 1, completedAt: -1 } — supports the profile page's reverse-chronological history
and heatmap queries.