Files
fitness/backend/schema.sql
T

354 lines
16 KiB
SQL

-- Fitness Tracking App - Production Database Schema
-- Database: fitness_app
-- Created: 2026-04-21
-- Create Database
CREATE DATABASE IF NOT EXISTS fitness_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE fitness_app;
-- Users Table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL COMMENT 'Unique identifier for the user',
phone_e164 VARCHAR(16) UNIQUE COMMENT 'Verified phone number in E.164 format',
phone_verified_at DATETIME NULL,
username VARCHAR(50) UNIQUE,
email VARCHAR(100) UNIQUE,
password_hash VARCHAR(255) NULL COMMENT 'Optional legacy password hash',
api_key VARCHAR(64) UNIQUE COMMENT 'Optional legacy API key',
api_secret VARCHAR(64) NULL COMMENT 'Optional legacy HMAC secret',
full_name VARCHAR(100),
avatar_url VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
account_role ENUM('member', 'owner', 'content_manager', 'support') NOT NULL DEFAULT 'member',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_uuid (uuid),
INDEX idx_api_key (api_key),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE app_settings (
setting_key VARCHAR(100) PRIMARY KEY,
setting_value JSON NOT NULL,
is_public BOOLEAN NOT NULL DEFAULT FALSE,
revision BIGINT UNSIGNED NOT NULL DEFAULT 1,
updated_by INT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_settings_public (is_public, setting_key),
CONSTRAINT fk_settings_editor FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE app_setting_audit (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) NOT NULL,
previous_value JSON NULL,
new_value JSON NOT NULL,
actor_user_id INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_settings_audit_key_time (setting_key, created_at),
INDEX idx_settings_audit_actor_time (actor_user_id, created_at),
CONSTRAINT fk_settings_audit_actor FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Curated workout library. GIFs are optional, versioned media URLs, not uploaded inline.
CREATE TABLE exercises (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
exercise_uuid CHAR(36) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE,
title_ar VARCHAR(160) NOT NULL,
title_en VARCHAR(160) NULL,
instructions_ar JSON NOT NULL,
target_muscles JSON NOT NULL,
equipment JSON NOT NULL,
difficulty ENUM('beginner', 'intermediate', 'advanced') NOT NULL DEFAULT 'beginner',
movement_type ENUM('strength', 'mobility', 'cardio', 'recovery') NOT NULL,
gif_url VARCHAR(500) NULL,
gif_poster_url VARCHAR(500) NULL,
duration_seconds SMALLINT UNSIGNED NULL,
repetitions VARCHAR(80) NULL,
safety_notes_ar JSON NOT NULL,
alternative_exercise_id BIGINT UNSIGNED NULL,
is_published BOOLEAN NOT NULL DEFAULT FALSE,
content_revision INT UNSIGNED NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_exercises_public (is_published, movement_type, difficulty),
CONSTRAINT fk_exercise_alternative FOREIGN KEY (alternative_exercise_id) REFERENCES exercises(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE training_plans (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
plan_uuid CHAR(36) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE,
title_ar VARCHAR(160) NOT NULL,
summary_ar TEXT NOT NULL,
goal ENUM('general_fitness', 'weight_management', 'mobility', 'endurance') NOT NULL,
level ENUM('beginner', 'intermediate', 'advanced') NOT NULL,
weeks_duration TINYINT UNSIGNED NOT NULL,
source_notes JSON NULL,
safety_notes_ar JSON NOT NULL,
is_published BOOLEAN NOT NULL DEFAULT FALSE,
content_revision INT UNSIGNED NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_plans_public (is_published, goal, level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE training_plan_sessions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
plan_id BIGINT UNSIGNED NOT NULL,
week_number TINYINT UNSIGNED NOT NULL,
day_number TINYINT UNSIGNED NOT NULL,
title_ar VARCHAR(160) NOT NULL,
session_type ENUM('strength', 'walking', 'mobility', 'rest') NOT NULL,
duration_minutes SMALLINT UNSIGNED NOT NULL DEFAULT 0,
intensity ENUM('easy', 'moderate', 'vigorous') NOT NULL DEFAULT 'easy',
notes_ar TEXT NULL,
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
UNIQUE KEY uq_plan_week_day (plan_id, week_number, day_number),
CONSTRAINT fk_plan_sessions_plan FOREIGN KEY (plan_id) REFERENCES training_plans(id) ON DELETE CASCADE,
INDEX idx_plan_sessions_order (plan_id, week_number, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE training_session_exercises (
session_id BIGINT UNSIGNED NOT NULL,
exercise_id BIGINT UNSIGNED NOT NULL,
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
sets TINYINT UNSIGNED NULL,
reps VARCHAR(60) NULL,
duration_seconds SMALLINT UNSIGNED NULL,
rest_seconds SMALLINT UNSIGNED NOT NULL DEFAULT 45,
PRIMARY KEY (session_id, exercise_id),
CONSTRAINT fk_session_exercises_session FOREIGN KEY (session_id) REFERENCES training_plan_sessions(id) ON DELETE CASCADE,
CONSTRAINT fk_session_exercises_exercise FOREIGN KEY (exercise_id) REFERENCES exercises(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE training_content_audit (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
entity_type ENUM('exercise', 'plan') NOT NULL,
entity_uuid CHAR(36) NOT NULL,
action ENUM('create', 'update', 'publish', 'unpublish') NOT NULL,
previous_value JSON NULL,
new_value JSON NOT NULL,
actor_user_id INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_content_audit_entity_time (entity_type, entity_uuid, created_at),
INDEX idx_content_audit_actor_time (actor_user_id, created_at),
CONSTRAINT fk_content_audit_actor FOREIGN KEY (actor_user_id)
REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE meal_entries (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
client_meal_uuid CHAR(36) NOT NULL,
name VARCHAR(180) NOT NULL,
meal_type ENUM('breakfast', 'lunch', 'dinner', 'snack') NOT NULL,
calories DECIMAL(8,2) NOT NULL,
protein_grams DECIMAL(7,2) NOT NULL DEFAULT 0,
carbohydrate_grams DECIMAL(7,2) NOT NULL DEFAULT 0,
fat_grams DECIMAL(7,2) NOT NULL DEFAULT 0,
consumed_at DATETIME NOT NULL,
source ENUM('manual', 'photo_ai') NOT NULL DEFAULT 'manual',
notes VARCHAR(1000) NOT NULL DEFAULT '',
revision INT UNSIGNED NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_meal_user_client_uuid (user_id, client_meal_uuid),
INDEX idx_meals_user_consumed (user_id, consumed_at DESC),
CONSTRAINT fk_meals_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CHECK (calories >= 0 AND calories <= 10000),
CHECK (protein_grams >= 0 AND protein_grams <= 1000),
CHECK (carbohydrate_grams >= 0 AND carbohydrate_grams <= 1000),
CHECK (fat_grams >= 0 AND fat_grams <= 1000)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Phone OTP challenges contain keyed digests, never the plaintext code.
CREATE TABLE otp_challenges (
challenge_uuid CHAR(36) PRIMARY KEY,
phone_e164 VARCHAR(16) NOT NULL,
purpose ENUM('register', 'login', 'change_phone') NOT NULL,
code_digest CHAR(64) NOT NULL,
attempt_count TINYINT UNSIGNED NOT NULL DEFAULT 0,
max_attempts TINYINT UNSIGNED NOT NULL DEFAULT 5,
request_ip_digest CHAR(64) NULL,
device_uuid CHAR(36) NULL,
expires_at DATETIME NOT NULL,
consumed_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_otp_phone_created (phone_e164, created_at),
INDEX idx_otp_expiry (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE auth_rate_limit_buckets (
bucket_digest CHAR(64) PRIMARY KEY,
bucket_type ENUM('phone', 'ip', 'device') NOT NULL,
window_started_at DATETIME NOT NULL,
request_count INT UNSIGNED NOT NULL DEFAULT 0,
blocked_until DATETIME NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_rate_bucket_expiry (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE user_devices (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
device_uuid CHAR(36) NOT NULL,
platform ENUM('ios', 'android', 'web') NOT NULL,
public_key TEXT NULL,
key_fingerprint CHAR(64) NULL UNIQUE,
key_algorithm VARCHAR(32) NULL,
display_name VARCHAR(80) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at DATETIME NULL,
revoked_at DATETIME NULL,
UNIQUE KEY uq_device_user_uuid (user_id, device_uuid),
INDEX idx_devices_user_active (user_id, revoked_at),
CONSTRAINT fk_devices_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE auth_sessions (
session_uuid CHAR(36) PRIMARY KEY,
family_uuid CHAR(36) NOT NULL,
user_id INT NOT NULL,
device_uuid CHAR(36) NULL,
refresh_token_digest CHAR(64) NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
replaced_by CHAR(36) NULL,
INDEX idx_sessions_user_active (user_id, revoked_at, expires_at),
INDEX idx_sessions_family (family_uuid),
CONSTRAINT fk_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_sessions_device FOREIGN KEY (user_id, device_uuid)
REFERENCES user_devices(user_id, device_uuid) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Workouts Table
CREATE TABLE IF NOT EXISTS workouts (
id INT AUTO_INCREMENT PRIMARY KEY,
workout_uuid CHAR(36) UNIQUE NOT NULL COMMENT 'Unique identifier for the workout',
user_id INT NOT NULL,
client_workout_uuid CHAR(36) NULL COMMENT 'Client-generated idempotency key',
workout_type ENUM('running', 'walking') NOT NULL,
distance_meters INT NOT NULL COMMENT 'Total distance in meters',
duration_seconds INT NOT NULL COMMENT 'Total duration in seconds',
elevation_gain_meters INT DEFAULT 0 COMMENT 'Elevation gain in meters',
elevation_loss_meters INT DEFAULT 0 COMMENT 'Elevation loss in meters',
calories_burned FLOAT DEFAULT 0,
average_pace_mps FLOAT COMMENT 'Average pace in meters per second',
max_speed_mps FLOAT DEFAULT 0 COMMENT 'Max speed in meters per second',
route_polyline LONGTEXT NOT NULL COMMENT 'Encoded polyline format (Google Maps Polyline Algorithm)',
coordinate_count INT NOT NULL COMMENT 'Total number of GPS coordinates recorded',
start_lat DECIMAL(10, 8) NOT NULL,
start_lng DECIMAL(11, 8) NOT NULL,
end_lat DECIMAL(10, 8) NOT NULL,
end_lng DECIMAL(11, 8) NOT NULL,
start_time DATETIME NOT NULL COMMENT 'ISO 8601 datetime when workout started',
end_time DATETIME NOT NULL COMMENT 'ISO 8601 datetime when workout ended',
weather_condition VARCHAR(50),
temperature_celsius FLOAT,
notes TEXT,
is_public BOOLEAN DEFAULT FALSE,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_workout_uuid (workout_uuid),
INDEX idx_user_id (user_id),
INDEX idx_workout_type (workout_type),
INDEX idx_synced_at (synced_at),
INDEX idx_created_at (created_at),
INDEX idx_user_created (user_id, created_at),
UNIQUE KEY uq_user_client_workout (user_id, client_workout_uuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Workout Segments Table (for detailed route tracking if needed)
CREATE TABLE IF NOT EXISTS workout_segments (
id INT AUTO_INCREMENT PRIMARY KEY,
workout_id INT NOT NULL,
segment_order INT NOT NULL COMMENT 'Order of segment in workout',
duration_seconds INT,
distance_meters INT,
average_pace_mps FLOAT,
index_in_polyline INT COMMENT 'Start index in polyline',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (workout_id) REFERENCES workouts(id) ON DELETE CASCADE,
INDEX idx_workout_id (workout_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- API Logs Table (for debugging and analytics)
CREATE TABLE IF NOT EXISTS api_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
endpoint VARCHAR(255),
method VARCHAR(10),
status_code INT,
request_hash VARCHAR(64) COMMENT 'Hash of request for deduplication',
ip_address VARCHAR(45),
user_agent VARCHAR(255),
response_time_ms INT,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at),
INDEX idx_endpoint (endpoint),
INDEX idx_request_hash (request_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Statistics Cache Table (for performance optimization)
CREATE TABLE IF NOT EXISTS user_stats_cache (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT UNIQUE NOT NULL,
total_workouts INT DEFAULT 0,
total_distance_meters INT DEFAULT 0,
total_duration_seconds INT DEFAULT 0,
total_calories_burned FLOAT DEFAULT 0,
average_pace_mps FLOAT,
last_workout_date DATETIME,
streak_days INT DEFAULT 0,
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_cached_at (cached_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create Triggers for audit trail
DELIMITER $$
CREATE TRIGGER workout_audit_insert AFTER INSERT ON workouts
FOR EACH ROW
BEGIN
INSERT INTO api_logs (endpoint, method, status_code, error_message, created_at)
VALUES ('workouts', 'INSERT', 201, NULL, NOW());
END $$
DELIMITER ;
-- Stored Procedure to get user workout statistics
DELIMITER $$
CREATE PROCEDURE GetUserStats(IN p_user_id INT)
BEGIN
SELECT
COUNT(*) as total_workouts,
SUM(distance_meters) as total_distance,
SUM(duration_seconds) as total_duration,
SUM(calories_burned) as total_calories,
AVG(average_pace_mps) as avg_pace,
MAX(created_at) as last_workout,
workout_type
FROM workouts
WHERE user_id = p_user_id
GROUP BY workout_type
ORDER BY created_at DESC;
END $$
DELIMITER ;
-- Initial indexes for optimal query performance
CREATE INDEX idx_workouts_stats ON workouts(user_id, workout_type, created_at);
CREATE INDEX idx_users_active ON users(is_active, created_at);