-- ============================================================ -- TabataFit Supabase Schema -- ============================================================ -- Run this SQL in Supabase SQL Editor to set up your database -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================================ -- TABLES -- ============================================================ -- Trainers table CREATE TABLE IF NOT EXISTS public.trainers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name TEXT NOT NULL, specialty TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#FF6B35', avatar_url TEXT, workout_count INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Workouts table CREATE TABLE IF NOT EXISTS public.workouts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), title TEXT NOT NULL, trainer_id UUID NOT NULL REFERENCES public.trainers(id) ON DELETE CASCADE, category TEXT NOT NULL CHECK (category IN ('full-body', 'core', 'upper-body', 'lower-body', 'cardio')), level TEXT NOT NULL CHECK (level IN ('Beginner', 'Intermediate', 'Advanced')), duration INTEGER NOT NULL CHECK (duration IN (4, 8, 12, 20)), calories INTEGER NOT NULL, rounds INTEGER NOT NULL, prep_time INTEGER NOT NULL DEFAULT 10, work_time INTEGER NOT NULL DEFAULT 20, rest_time INTEGER NOT NULL DEFAULT 10, equipment TEXT[] DEFAULT '{}', music_vibe TEXT NOT NULL CHECK (music_vibe IN ('electronic', 'hip-hop', 'pop', 'rock', 'chill')), exercises JSONB NOT NULL DEFAULT '[]', thumbnail_url TEXT, video_url TEXT, is_featured BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Collections table CREATE TABLE IF NOT EXISTS public.collections ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), title TEXT NOT NULL, description TEXT NOT NULL, icon TEXT NOT NULL, gradient TEXT[] DEFAULT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Collection-Workouts junction table CREATE TABLE IF NOT EXISTS public.collection_workouts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), collection_id UUID NOT NULL REFERENCES public.collections(id) ON DELETE CASCADE, workout_id UUID NOT NULL REFERENCES public.workouts(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL DEFAULT 0, UNIQUE(collection_id, workout_id) ); -- Admin users table CREATE TABLE IF NOT EXISTS public.admin_users ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('admin', 'editor')) DEFAULT 'editor', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_login TIMESTAMP WITH TIME ZONE ); -- ============================================================ -- INDEXES -- ============================================================ CREATE INDEX IF NOT EXISTS idx_workouts_trainer_id ON public.workouts(trainer_id); CREATE INDEX IF NOT EXISTS idx_workouts_category ON public.workouts(category); CREATE INDEX IF NOT EXISTS idx_workouts_level ON public.workouts(level); CREATE INDEX IF NOT EXISTS idx_workouts_is_featured ON public.workouts(is_featured); CREATE INDEX IF NOT EXISTS idx_workouts_created_at ON public.workouts(created_at DESC); CREATE INDEX IF NOT EXISTS idx_collection_workouts_collection_id ON public.collection_workouts(collection_id); CREATE INDEX IF NOT EXISTS idx_collection_workouts_workout_id ON public.collection_workouts(workout_id); -- ============================================================ -- FUNCTIONS -- ============================================================ -- Update timestamps automatically CREATE OR REPLACE FUNCTION public.update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; -- Create triggers for updated_at CREATE TRIGGER update_trainers_updated_at BEFORE UPDATE ON public.trainers FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER update_workouts_updated_at BEFORE UPDATE ON public.workouts FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER update_collections_updated_at BEFORE UPDATE ON public.collections FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); -- Update workout count for trainers CREATE OR REPLACE FUNCTION public.update_trainer_workout_count() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT') THEN UPDATE public.trainers SET workout_count = workout_count + 1 WHERE id = NEW.trainer_id; ELSIF (TG_OP = 'DELETE') THEN UPDATE public.trainers SET workout_count = workout_count - 1 WHERE id = OLD.trainer_id; ELSIF (TG_OP = 'UPDATE' AND OLD.trainer_id IS DISTINCT FROM NEW.trainer_id) THEN UPDATE public.trainers SET workout_count = workout_count - 1 WHERE id = OLD.trainer_id; UPDATE public.trainers SET workout_count = workout_count + 1 WHERE id = NEW.trainer_id; END IF; RETURN NULL; END; $$ language 'plpgsql'; CREATE TRIGGER update_trainer_workout_count_trigger AFTER INSERT OR UPDATE OR DELETE ON public.workouts FOR EACH ROW EXECUTE FUNCTION public.update_trainer_workout_count(); -- ============================================================ -- ROW LEVEL SECURITY (RLS) -- ============================================================ -- Enable RLS on all tables ALTER TABLE public.trainers ENABLE ROW LEVEL SECURITY; ALTER TABLE public.workouts ENABLE ROW LEVEL SECURITY; ALTER TABLE public.collections ENABLE ROW LEVEL SECURITY; ALTER TABLE public.collection_workouts ENABLE ROW LEVEL SECURITY; ALTER TABLE public.admin_users ENABLE ROW LEVEL SECURITY; -- Trainers policies CREATE POLICY "Allow public read access" ON public.trainers FOR SELECT USING (true); CREATE POLICY "Allow admin write access" ON public.trainers FOR ALL USING ( EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) ); -- Workouts policies CREATE POLICY "Allow public read access" ON public.workouts FOR SELECT USING (true); CREATE POLICY "Allow admin write access" ON public.workouts FOR ALL USING ( EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) ); -- Collections policies CREATE POLICY "Allow public read access" ON public.collections FOR SELECT USING (true); CREATE POLICY "Allow admin write access" ON public.collections FOR ALL USING ( EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) ); -- Collection_workouts policies CREATE POLICY "Allow public read access" ON public.collection_workouts FOR SELECT USING (true); CREATE POLICY "Allow admin write access" ON public.collection_workouts FOR ALL USING ( EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) ); -- Admin users policies CREATE POLICY "Allow admin self access" ON public.admin_users FOR ALL USING (id = auth.uid()); -- ============================================================ -- STORAGE BUCKETS -- ============================================================ -- Create storage buckets (run in Supabase Dashboard or use supabase CLI) -- These need to be created via the UI or Storage API /* Bucket name: workout-videos Public: true Allowed MIME types: video/mp4, video/webm, video/quicktime Max file size: 500MB Bucket name: workout-thumbnails Public: true Allowed MIME types: image/jpeg, image/png, image/webp Max file size: 10MB Bucket name: trainer-avatars Public: true Allowed MIME types: image/jpeg, image/png, image/webp Max file size: 5MB */ -- ============================================================ -- SAMPLE DATA (Optional) -- ============================================================ -- Insert sample trainers INSERT INTO public.trainers (name, specialty, color) VALUES ('Emma', 'HIIT Specialist', '#FF6B35'), ('Jake', 'Strength Coach', '#5AC8FA'), ('Alex', 'Cardio Expert', '#30D158'), ('Sarah', 'Yoga & Mobility', '#FF9500'), ('Mike', 'CrossFit Trainer', '#AF52DE') ON CONFLICT DO NOTHING; -- Insert sample collections INSERT INTO public.collections (title, description, icon, gradient) VALUES ('Quick Burns', 'Short workouts for busy days', 'flame', ARRAY['#FF6B35', '#FF9500']), ('Full Body Power', 'Complete body workouts', 'figure.strengthtraining.traditional', ARRAY['#5AC8FA', '#30D158']), ('Core Crusher', 'Focus on abdominal strength', 'target', ARRAY['#AF52DE', '#FF6B35']), ('Beginner Friendly', 'Perfect for getting started', 'star', ARRAY['#30D158', '#5AC8FA']), ('Advanced Challenge', 'Push your limits', 'bolt', ARRAY['#FF3B30', '#FF6B35']) ON CONFLICT DO NOTHING; -- ============================================================ -- ADMIN SETUP -- ============================================================ -- To add yourself as admin, run this after creating your account: -- INSERT INTO public.admin_users (id, email, role) -- SELECT id, email, 'admin' -- FROM auth.users -- WHERE email = 'your-email@example.com'; -- ============================================================ -- USER SYNC TABLES (Premium Feature - Anonymous Auth) -- ============================================================ -- User profiles (created after opt-in to sync for personalization) CREATE TABLE IF NOT EXISTS public.user_profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT, name TEXT NOT NULL, fitness_level TEXT NOT NULL, goal TEXT NOT NULL, weekly_frequency INTEGER NOT NULL, barriers JSONB DEFAULT '[]', is_anonymous BOOLEAN DEFAULT true, sync_enabled BOOLEAN DEFAULT true, subscription_plan TEXT DEFAULT 'premium-yearly', onboarding_completed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Workout sessions (synced history for personalization) CREATE TABLE IF NOT EXISTS public.workout_sessions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, workout_id UUID NOT NULL REFERENCES public.workouts(id), completed_at TIMESTAMP WITH TIME ZONE NOT NULL, duration_seconds INTEGER, calories_burned INTEGER, feeling_rating INTEGER CHECK (feeling_rating BETWEEN 1 AND 5), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- User preferences for personalization CREATE TABLE IF NOT EXISTS public.user_preferences ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, preferred_categories TEXT[] DEFAULT '{}', preferred_trainers TEXT[] DEFAULT '{}', preferred_durations INTEGER[] DEFAULT '{}', difficulty_preference TEXT DEFAULT 'matched', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(user_id) ); -- Indexes for performance CREATE INDEX IF NOT EXISTS idx_workout_sessions_user_id ON public.workout_sessions(user_id); CREATE INDEX IF NOT EXISTS idx_workout_sessions_completed_at ON public.workout_sessions(completed_at DESC); CREATE INDEX IF NOT EXISTS idx_user_profiles_email ON public.user_profiles(email); -- RLS Policies for user data protection ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY; ALTER TABLE public.workout_sessions ENABLE ROW LEVEL SECURITY; ALTER TABLE public.user_preferences ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users manage own profile" ON public.user_profiles FOR ALL USING (id = auth.uid()); CREATE POLICY "Users manage own sessions" ON public.workout_sessions FOR ALL USING (user_id = auth.uid()); CREATE POLICY "Users manage own preferences" ON public.user_preferences FOR ALL USING (user_id = auth.uid()); -- Triggers for updated_at CREATE TRIGGER update_user_profiles_updated_at BEFORE UPDATE ON public.user_profiles FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER update_user_preferences_updated_at BEFORE UPDATE ON public.user_preferences FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();