-- 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; -- 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);