Why a Fitness App
Fitness apps are a genuinely good project to build if you want to touch a full stack in a way that isn't a toy: you need real auth and user state, interactive UI that isn't just forms, and something more interesting than a CRUD wrapper for the "smart" part of the app. That's what pushed me to build Elevate Fitness — a Next.js app with Supabase, ShadCN, and Tailwind CSS that generates personalized workout and diet plans, calculates BMI, counts calories, and shows exercises through interactive SVG demos. It's live at elevate-your-fitness.vercel.app.
This post covers the architecture decisions and the parts that were actually hard to get right.
App Router Structure
I used Next.js App Router from the start rather than Pages Router, mainly to get server components for data-heavy pages (plan history, exercise library) while keeping interactive pieces (the SVG demos, the plan generator form) as client components.
app/
(auth)/
login/page.tsx
signup/page.tsx
dashboard/
page.tsx // server component, fetches user + plan summary
workout/[day]/page.tsx
tools/
bmi-calculator/page.tsx
calorie-counter/page.tsx
api/
generate-plan/route.ts
exercises/[slug]/route.ts
components/
exercise-svg/
plan-generator/
lib/
supabase/client.ts
supabase/server.ts
Splitting lib/supabase/client.ts and lib/supabase/server.ts matters more than it sounds — Supabase's auth helpers behave differently depending on whether you're reading a session in a server component versus a client component, and mixing them up is the single most common bug I hit early on (stale sessions, or a server component that silently reads null for a logged-in user).
Supabase for Auth and State
Supabase handles auth, the plan history table, and user profile data (age, weight, goal, activity level — the inputs the plan generator needs). The schema for a generated plan looks roughly like:
create table workout_plans (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id),
goal text not null,
days jsonb not null, -- structured plan: [{day, exercises: [...]}]
diet_summary jsonb,
created_at timestamp default now()
);
Storing the plan as jsonb rather than normalizing every exercise into its own row was a deliberate tradeoff — plans are read as a whole unit (show me today's workout) far more often than queried by individual exercise, so keeping the structure denormalized avoids a join on every dashboard load.
SVG-Based Exercise Demos
Video demos are the obvious choice for showing an exercise, but they're heavy to host and slow to load on a fitness app people open on their phone mid-workout. I went with interactive SVG illustrations instead — each exercise is a set of SVG paths that animate through the movement's key positions using CSS transitions triggered by state.
function ExerciseDemo({ exercise }: { exercise: Exercise }) {
const [phase, setPhase] = useState<'start' | 'mid' | 'end'>('start');
useEffect(() => {
const sequence: typeof phase[] = ['start', 'mid', 'end', 'mid'];
let i = 0;
const interval = setInterval(() => {
i = (i + 1) % sequence.length;
setPhase(sequence[i]);
}, 900);
return () => clearInterval(interval);
}, []);
return (
<svg viewBox="0 0 200 200" className="w-full h-auto">
<path d={exercise.paths[phase]} className="transition-all duration-500 fill-current" />
</svg>
);
}
This keeps the exercise library lightweight (a JSON file of path definitions instead of a video CDN bill) and lets the demo scale cleanly at any screen size, which matters a lot on mobile where most people actually use a fitness app.
Generating Personalized Plans with AI
The plan generator is the core "AI" feature. It takes a user's goal, current stats, and activity level, and produces a structured multi-day plan plus a diet summary. The API route builds a constrained prompt and expects strict JSON back so the response can be rendered directly into the UI without a parsing layer that might break:
// app/api/generate-plan/route.ts
const prompt = `
Generate a 5-day workout plan and diet summary for:
Goal: ${goal}, Weight: ${weightKg}kg, Height: ${heightCm}cm, Activity: ${activityLevel}
Return strict JSON matching this shape:
{
"days": [{ "day": "Monday", "focus": "Upper Body", "exercises": [{"name": "", "sets": 0, "reps": 0}] }],
"diet_summary": { "calorie_target": 0, "protein_g": 0, "notes": "" }
}
No prose outside the JSON.
`;
The "no prose outside the JSON" instruction plus a JSON.parse wrapped in a try/catch with a retry is what actually made this reliable — LLMs will occasionally wrap valid JSON in a sentence or two of explanation, and the retry-on-parse-failure loop handles that without needing a heavier structured-output setup.
BMI Calculator and Calorie Counter
These are the "boring but necessary" utility tools, and they're intentionally simple — pure client-side calculation, no API call needed:
function calculateBMI(weightKg: number, heightCm: number): number {
const heightM = heightCm / 100;
return +(weightKg / (heightM * heightM)).toFixed(1);
}
They're useful on their own, but they also feed into the plan generator's prompt — a user's BMI and calorie target directly shape the diet summary the AI produces, so these "simple" tools aren't decorative, they're inputs to the smarter feature.
Exploring Agentic AI and MCP Servers
The plan generator today is a single prompt-response call, but I've been exploring turning it into something closer to an actual agent — using MCP (Model Context Protocol) servers to give the plan generator access to tools rather than just a static prompt. Concretely, that means giving the model callable tools like "look up the user's past week of logged workouts" or "check if a suggested exercise conflicts with a stated injury," so the plan isn't generated from a single snapshot of form inputs, but from actual context it can query as needed.
The direction I'm exploring is roughly:
User request
│
▼
Agent (LLM) ──▶ MCP Tool: get_user_history()
│ ──▶ MCP Tool: check_injury_constraints()
│ ──▶ MCP Tool: get_available_equipment()
▼
Structured plan generation (grounded in tool results, not just form inputs)
This is still exploratory rather than fully shipped, but it's the natural next step for making the "personalized" part of a personalized fitness app actually mean something beyond plugging numbers into a prompt template.
What I'd Improve Next
The biggest gap right now is plan adaptation — the app generates a plan but doesn't yet adjust it based on whether the user actually completed the previous week's workouts. That's exactly the kind of context an MCP-based agent setup would close, which is why it's the direction I'm pushing the project next.
Try It or Reach Out
You can try Elevate Fitness at elevate-your-fitness.vercel.app. If you want to talk about the Next.js/Supabase setup, the SVG demo approach, or the agentic AI direction, reach me at rishabnishad22@gmail.com, on WhatsApp, or through my contact page.