Browse Templates/Fitness Tracker

Fitness Tracker

A comprehensive fitness tracker database schema designed for mobile health and workout applications. Includes user profiles, workout sessions with duration and calories tracking, an exercise library, many-to-many workout-exercise relationships with sets/reps/weight data, and personal fitness goals with progress tracking. Ideal for apps like Strava, Nike Run Club, or custom fitness solutions.

PostgreSQL10 tablesMobile
fitnesshealthworkoutmobiletracking
Created with ERD Studio

About this schema

exercises is a shared library and workout_exercises is one person doing one of them. The library row knows the muscle group and the difficulty; the link row knows the sets, the reps and the weight lifted that day. Separating them is what makes progress on a single lift a query rather than a search through free text.

body_metrics is a time series, not a profile. users carries height and a current weight for convenience, while every measurement ever taken lives as its own row with a recorded_at — because the whole point of tracking is the shape of the line, not the last value.

Calories appear twice with different meanings. workout_types.calories_per_minute is an estimate used to fill in a number; workouts.calories_burned is what was recorded for that session. The stored value survives the estimate being revised, which is what keeps last year's totals stable.

Key relationships

users → workouts → workout_exercises → exercises
Session, then the movements in it. sort_order preserves the order they were performed, which matters for how a workout is read back.
workout_types → workouts
The category supplies an icon and a calories_per_minute estimate, so a session gets a plausible number without a heart-rate monitor.
users → body_metrics
One row per measurement with weight, body fat and BMI together, so a chart comes from a single ordered read.
users → goals
target_value against current_value on the row, so progress is a comparison rather than a recalculation over the activity history.
achievements → user_achievements → users
The catalogue and the unlock, with unlocked_at recording the moment rather than a boolean that forgets when.
users → nutrition_logs
Per-meal entries with macros, keyed only by the user and a timestamp, so a day is a date range rather than a parent row.

Design decisions

Exercises are a library, not free text

Letting someone type bench press each time is faster to build and makes every progress chart impossible — three spellings become three exercises. A shared table means sets and weights accumulate against one row, muscle-group filtering works, and the cost is that an exercise nobody has added yet cannot be logged until the library grows.

goals.current_value is stored

A goal like run 100km this month could be computed from workouts on every view. Storing the running value makes the goal screen one read and lets goals exist that the app cannot derive at all — a target weight, a habit, something logged elsewhere. The cost is that it must be updated as activity lands, and a corrected workout leaves it stale until recomputed.

Nutrition logs have no parent meal row

Each food is its own row with a meal_type and a timestamp, so a day is assembled by date rather than by a meals table. That keeps logging a single insert and makes a daily total a simple sum. What it gives up is the meal as an object — you cannot name, save or repeat yesterday's lunch without matching rows by time and type.

Body metrics are never overwritten

users.weight_kg is the convenient current value; body_metrics is the history. Keeping only the profile column would make the app a scale rather than a tracker. Both means a write updates two places, with the time series as the authoritative record the profile can always be restored from.

Tables in this template

usersUsers
8 cols
workout_typesWorkout Types
4 cols
workoutsWorkouts
9 cols
exercisesExercises
6 cols
workout_exercisesWorkout Exercises
8 cols
goalsGoals
9 cols
achievementsAchievements
6 cols
user_achievementsUser Achievements
4 cols
body_metricsBody Metrics
7 cols
nutrition_logsNutrition Logs
9 cols

Frequently asked

How do I chart progress on one exercise?
Join workout_exercises to workouts for that user and exercise, ordered by workout_date, and plot weight_kg or reps. That is only possible because the exercise is a shared row rather than a typed name.
Why is calories_burned stored on the workout?
So the number stays what it was. Recomputing from calories_per_minute would restate every past session the moment an estimate is tuned, and it could not accommodate a figure that came from a watch.
How are goals of different kinds handled?
Through goal_type with a numeric target_value, which covers distance, weight, frequency and duration in one table. The interpretation of the number lives in the application, so a new goal type needs no schema change.
Where does a workout plan or routine live?
Nowhere here — workouts record what was done, not what was planned. A routine needs its own template tables, with a session created from a template when the user starts it.
Should BMI be stored or calculated?
It is stored on the metric row, alongside the weight it was derived from. That keeps the historical value consistent even if the height on the profile is later corrected, which would otherwise silently change every past BMI.