Skip to main content

Overview

Creates a new personalized workout and meal plan for a user. This endpoint uses a PostgreSQL function (create_user_plan) that automatically handles:
  • Deactivating any existing active plans
  • Setting appropriate expiration dates (5 weeks for free, 1 year for paid)
  • Updating the user’s profile with plan metadata
  • Validating freemium constraints

Authentication

Requires authenticated user via Supabase Auth. Uses Row Level Security (RLS) policies.

Endpoint

RPC: create_user_plan(user_uuid, p_plan_data, p_plan_type)
This is a Supabase RPC function call, not a REST endpoint.

Request Parameters

user_uuid
UUID
required
The authenticated user’s UUID from Supabase Auth
p_plan_data
JSONB
required
The complete WizardState object containing all plan configuration. See WizardState structure below.
p_plan_type
string
default:"free"
Plan type: "free" or "paid". Defaults to "free".

WizardState Structure

The p_plan_data parameter must contain a complete WizardState object:
interface WizardState {
  currentStep: number;                    // Current wizard step (0-8)
  level: TrainingLevel | null;            // "principiante" | "basico" | "intermedio" | "avanzado" | "elite"
  goal: TrainingGoal | null;              // "perder_grasa" | "ganar_musculo" | "tonificar" | "resistencia" | etc.
  time: number;                           // Session duration in minutes (e.g., 30, 45, 60)
  equipment: EquipmentType[];             // Array of available equipment
  duration: ProgramDuration | null;       // "1_dia" | "3_dias" | "1_semana" | "2_semanas" | "1_mes" | "6_semanas" | "2_meses" | "3_meses"
  selectedExercises: string[];            // Array of exercise IDs
  selectedFoods: string[];                // Array of food preferences
  userName: string;                       // User's name for personalization
  userBodyData: UserBodyData | null;      // Body metrics and goals
}

interface UserBodyData {
  currentWeight: number;                  // Current weight in kg
  targetWeight: number;                   // Target weight in kg
  height: number;                         // Height in cm
  age: number;                            // Age in years
  gender: "masculino" | "femenino";      // Gender
  activityLevel: ActivityLevel;           // "sedentario" | "ligero" | "moderado" | "activo" | "muy_activo"
  weightGoal: WeightGoal;                 // "perder" | "mantener" | "ganar"
}

Training Levels

  • principiante - Beginner, no prior experience
  • basico - Basic, 1-3 months experience
  • intermedio - Intermediate, 3-12 months experience
  • avanzado - Advanced, 1+ years experience
  • elite - Elite athlete level

Training Goals

  • perder_grasa - Fat loss
  • ganar_musculo - Muscle gain
  • tonificar - Toning/definition
  • resistencia - Endurance
  • flexibilidad - Flexibility
  • fuerza - Pure strength
  • energia - Energy/vitality
  • salud - General health

Equipment Types

  • sin_equipo - No equipment (bodyweight)
  • gym_completo - Full gym access
  • mancuernas - Dumbbells
  • bandas - Resistance bands
  • barra - Barbell
  • banco - Bench
  • pull_up_bar - Pull-up bar
  • kettlebell - Kettlebell
  • maquinas - Machines
  • trx - TRX suspension
  • Plus more (see source code for complete list)

Freemium Logic

Free Plan Rules

  • Users can create one free plan (lifetime)
  • Free plans last 5 weeks from creation
  • Once a free plan is created, has_free_plan_used is set to true
  • Users cannot create another free plan after using their free one
  • Requires active subscription (checked via has_active_subscription(user_uuid))
  • Paid plans last 1 year from creation
  • Paid users can create unlimited plans
  • Each new plan deactivates the previous one

Validation

Before creating a plan, the function checks:
  1. Has active subscription? → If yes, allow paid plan
  2. Already has active plan? → Return error already_has_plan
  3. Already used free plan? → Return error free_used
  4. Otherwise → Allow free plan creation

Response

plan_id
UUID
The ID of the newly created plan

Example Usage

import { planService } from "@/features/plans/services/plan-service";
import type { WizardState } from "@/features/wizard/types";

const wizardState: WizardState = {
  currentStep: 8,
  level: "intermedio",
  goal: "ganar_musculo",
  time: 45,
  equipment: ["mancuernas", "banco", "barra"],
  duration: "1_mes",
  selectedExercises: ["bench_press", "squats", "deadlifts"],
  selectedFoods: ["chicken", "rice", "broccoli"],
  userName: "Juan",
  userBodyData: {
    currentWeight: 75,
    targetWeight: 80,
    height: 175,
    age: 28,
    gender: "masculino",
    activityLevel: "moderado",
    weightGoal: "ganar"
  }
};

const result = await planService.createPlan(
  userId,
  wizardState,
  "free" // or "paid"
);

if (result.success) {
  console.log("Plan created:", result.planId);
} else {
  console.error("Error:", result.error);
}

Database Schema

CREATE TABLE user_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  plan_data JSONB NOT NULL,           -- WizardState serialized
  plan_type TEXT NOT NULL DEFAULT 'free' CHECK (plan_type IN ('free', 'paid')),
  created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,    -- created_at + duration
  is_active BOOLEAN DEFAULT TRUE,
  download_count INTEGER DEFAULT 0,   -- Only paid plans can increment
  updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

Error Responses

error
string
Error message if plan creation fails

Common Errors

  • "Ya tienes un plan activo" - User already has an active plan
  • "Ya usaste tu plan gratuito. Actualiza a premium para crear más planes." - Free plan already used
  • "No se pudo verificar tu cuenta" - Authentication error

Build docs developers (and LLMs) love