AI-Powered Fitness Assistant Backend
An intelligent fitness coaching system that provides personalized workout recommendations using AI-driven conversational interfaces, vector similarity search, and real-time data synchronization.
π Table of Contents
- Overview
- Architecture
- Technologies
- Key Features
- System Flows
- Internal Processes
- Data Flow
- Setup & Installation
- Deployment
- API Documentation
- Database Schema
π― Overview
This backend service powers an AI fitness assistant that:
- Collects user profiles through natural conversation (age, weight, height, goals, injuries, etc.)
- Generates personalized workouts using AI and vector similarity search
- Tracks workout performance with detailed metrics and AI-generated summaries
- Learns from past sessions to improve future recommendations
- Auto-syncs data between PostgreSQL and Qdrant vector database in real-time
ποΈ Architecture
System Components
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Application β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β HTTP/REST
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β Express.js API Server β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Workflow Engine (workflow.ts) β β
β β β’ Profile Intake β’ Exercise Recommendation β β
β β β’ Confirmations β’ Summary Generation β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββ
β β β β
β Gemini AI β PostgreSQL β Qdrant β Redis
β (LLM + Embed)β (Primary DB) β (Vector DB) β (Sessions)
β β β β
βββΌβββββββββββ βββΌβββββββββββ βββΌβββββββββββ βββΌβββββββββββ
β Google β β PostgreSQL β β Qdrant β β Redis β
β Gemini β β β β β β β
β - 2.0 β β - Users β β - Exercise β β - Session β
β Flash β β - Exercise β β Vectors β β State β
β - Text β β - Sessions β β - Workout β β - History β
β Embed β β - Results β β Vectors β β - TTL 24h β
β 004 β β β β β β β
ββββββββββββββ βββββββ¬βββββββ ββββββββββββββ ββββββββββββββ
β
ββββββββΌββββββββ
β PostgreSQL β
β LISTEN/ β
β NOTIFY β
β Triggers β
ββββββββ¬ββββββββ
β
ββββββββΌββββββββββββββββ
β Trigger Listener β
β (triggerListener.ts)β
ββββββββ¬ββββββββββββββββ
β
βββββββββββββ΄βββββββββββββββ
β β
ββββββΌβββββββββ ββββββββββΌββββββ
β Exercise β β Workout β
β Sync β β Sync β
β (DB β Vec) β β (DB β Vec) β
βββββββββββββββ ββββββββββββββββ
Component Responsibilities
Express.js API Server (src/index.ts, src/app.ts)
- Handles HTTP requests and routing
- Manages connections to all services
- Graceful shutdown handling
Workflow Engine (src/services/workflow.ts)
- Orchestrates multi-step conversational flow
- Manages state transitions
- Integrates AI responses with data validation
Session Manager (src/services/session.ts)
- Redis-based session storage
- 24-hour TTL for automatic cleanup
- Maintains conversation history and user context
Qdrant Services (src/services/qdrant.ts)
- Initializes vector collections
- Manages exercise and workout embeddings
Sync Services
- Exercise Sync (
src/services/exerciseSync.ts): Syncs exercises from PostgreSQL to Qdrant - Workout Sync (
src/services/workoutSync.ts): Syncs completed workouts and searches similar sessions
Trigger Listener (src/services/triggerListener.ts)
- Listens to PostgreSQL NOTIFY events
- Automatically triggers sync operations
- Reconnects on connection loss
Utilities (src/utils.ts)
- Text embedding generation
- TOON format encoding/decoding (compact JSON alternative)
- Profile data extraction and validation
- AI summary generation
- Retry logic for reliability
π οΈ Technologies
Core Stack
| Technology | Purpose | Version |
|---|---|---|
| Node.js | Runtime environment | 22+ |
| TypeScript | Type-safe JavaScript | 5.7.2 |
| Express | HTTP server framework | 4.21.1 |
Databases & Storage
| Technology | Purpose | Details |
|---|---|---|
| PostgreSQL | Primary database | 18-alpine, stores exercises, sessions, results |
| Qdrant | Vector database | Stores embeddings for similarity search |
| Redis | Session cache | 7-alpine, 24h TTL for session state |
AI & Machine Learning
| Technology | Purpose | Details |
|---|---|---|
| Google Gemini 2.0 Flash | LLM for conversations | Profile intake, recommendations, summaries |
| Text-Embedding-004 | Text embeddings | 768-dim vectors for similarity search |
| TOON Format | Compact data encoding | More efficient than JSON for AI prompts |
Infrastructure
| Technology | Purpose | Details |
|---|---|---|
| Docker | Containerization | Multi-service orchestration |
| Nginx | Reverse proxy | SSL termination, load balancing |
| PM2 | Process manager | Production app management |
β¨ Key Features
1. AI-Powered Conversational Interface
- Natural language profile collection
- Context-aware responses
- Multi-turn conversation handling
- Confirmation/cancellation flows
2. Intelligent Exercise Recommendations
- Vector similarity search for exercise matching
- Injury-aware filtering (excludes affected body parts)
- Past workout analysis for personalized suggestions
- Difficulty level adaptation
3. Real-Time Data Synchronization
- PostgreSQL triggers automatically notify changes
- Background listener processes sync events
- Exercises sync to Qdrant on INSERT/UPDATE/DELETE
- Workout sessions sync on completion
- Retry logic for reliability
4. Personalized Workout Learning
- Stores past workout performance
- Searches similar sessions using vector embeddings
- Uses historical data to improve recommendations
- Considers accuracy scores, mistakes, and completion rates
5. Comprehensive Performance Tracking
- Per-exercise metrics (reps, duration, calories, mistakes)
- Overall session metrics (completion %, accuracy, efficiency)
- AI-generated encouraging summaries
- Mistake tracking with counts
π System Flows
Main User Workflow (Sequential)
These flows represent the sequential journey a user takes from starting a conversation to completing a workout.
Flow 1: Profile Intake
βββββββββββββββ
β User β
β "I want β
β to start" β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Workflow: PROFILE_INTAKE β
β β
β 1. Check existing session (Redis) β
β 2. Create/load session state β
β 3. Send to Gemini AI with prompt β
β 4. Extract profile data if presentβ
β 5. Validate completeness β
ββββββββ¬βββββββββββββββββββββββββββββββ
β
βββ Incomplete βββ
β β
β βΌ
β ββββββββββββββββ
β β Ask for more β
β β information β
β ββββββββ¬ββββββββ
β β
ββββββββββββββββββ
β
βββ Complete
β
βΌ
ββββββββββββββββββββββββββββββββββββββ
β Workflow: PROFILE_CONFIRMATION β
β β
β 1. Store profile in session β
β 2. Present data to user β
β 3. Request confirmation β
ββββββββ¬βββββββββββββββββββββββββββββββ
β
βββ User: "CONFIRM"
β
βΌ
ββββββββββββββββββββββββββββββββββββββ
β Proceed to Exercise β
β Recommendation β
βββββββββββββββββββββββββββββββββββββββ
Flow 2: Exercise Recommendation
βββββββββββββββββββββββββββββββββββββββ
β Workflow: EXERCISE_RECOMMENDATION β
ββββββββ¬βββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Step 1: Search Query Refinement β
β β
β β’ Send profile + injuries to AI β
β β’ AI generates optimized query β
β β’ AI identifies body parts to β
β exclude based on injuries β
β β
β Example: β
β Injury: "knee pain" β
β β Exclude: ["knees", "legs"] β
β β Query: "upper body strength" β
ββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Step 2: Find Similar Past Workouts β
β β
β β’ Embed refined query (768-dim) β
β β’ Search Qdrant workout_sessions β
β β’ Filter by user_id β
β β’ Get top 5 similar sessions β
ββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Step 3: Retrieve Past Performance β
β β
β β’ Query PostgreSQL for session β
β details (exercises, metrics) β
β β’ Include: accuracy, mistakes, β
β completion %, calories burned β
β β’ Encode to TOON format (compact) β
ββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Step 4: Search Exercise Database β
β β
β β’ Search Qdrant exercises β
β β’ Use refined query embedding β
β β’ Apply body part filters β
β β’ Get top 50 candidates β
ββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Step 5: AI Workout Generation β
β β
β Prompt includes: β
β β’ Profile data (TOON) β
β β’ Similar sessions data (TOON) β
β β’ Available exercises (TOON) β
β β
β AI decides: β
β β’ Which exercises to include β
β β’ Reps/duration for each β
β β’ Rest periods β
β β’ Exercise order β
β β’ Warm-up/cool-down phases β
ββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Step 6: Exercise Validation β
β β
β β’ Parse AI response (TOON/JSON) β
β β’ Validate each exercise ID β
β β’ Fetch full details from DB β
β β’ Ensure data integrity β
β β’ Skip invalid exercises β
ββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Workflow: EXERCISE_CONFIRMATION β
β β
β β’ Store recommendations in session β
β β’ Present to user β
β β’ Request confirmation β
ββββββββββββββββββββββββββββββββββββββββ
Flow 3: Workout Completion
βββββββββββββββββββββββββββββββββββ
β User completes workout β
β Mobile app sends results β
ββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β POST /api/workflow β
β step: EXERCISE_SUMMARY β
β β
β Body (as JSON string): β
β { β
β target_duration_seconds: 1800, β
β completed_reps_count: 85, β
β target_reps_count: 100, β
β calories_burned: 250.5, β
β completion_percentage: 85.0, β
β total_mistakes: 12, β
β accuracy_score: 88.5, β
β efficiency_score: 82.3, β
β total_exercise: 5, β
β exercises: [ β
β { β
β exercise_id: "ex_001", β
β exercise_title: "Squats", β
β time_spent: 300, β
β repeats: 3, β
β total_reps: 36, β
β calories: 80.2, β
β mistakes: [...], β
β average_accuracy: 0.92 β
β }, β
β ... β
β ], β
β notes: "Felt good overall" β
β } β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Workflow: processExerciseSummary β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Step 1: Generate AI Summary β
β β
β β’ Send results to Gemini β
β β’ AI analyzes performance β
β β’ Creates encouraging summary β
β β’ Highlights strengths/improvements β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Step 2: PostgreSQL Transaction β
β β
β BEGIN; β
β β
β 1. INSERT past_sessions β
β β Returns session ID (UUID) β
β β
β 2. INSERT session_exercises β
β β Links exercises to session β
β β Maintains order β
β β
β 3. INSERT session_exercise_results β
β β Stores per-exercise metrics β
β β Stores mistakes as JSONB β
β β
β COMMIT; β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
β (Automatically triggers sync)
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL Trigger Fires β
β β past_session_insert_trigger β
β β NOTIFY past_session_changes β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Trigger Listener Receives Event β
β β Calls syncWorkoutSessionToQdrant() β
β β Stores workout embedding in Qdrant β
β β Tagged with user_id for filtering β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Step 3: Clear Session β
β β
β β’ Delete from Redis β
β β’ Session complete β
β β’ Ready for new workflow β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Return AI Summary to User β
β β
β Example: β
β "Great workout! You completed 85% β
β with 88.5% accuracy. Squats looked β
β excellent with 92% form. Watch for β
β hip sagging in push-ups. Keep it up!" β
ββββββββββββββββββββββββββββββββββββββββββββ
Background Processes (Automatic)
These processes run continuously in the background, independent of user interactions, to keep data synchronized across systems.
Flow 4: Real-Time Data Sync
ββββββββββββββββββββββββββββββββββββ
β PostgreSQL: INSERT/UPDATE/ β
β DELETE on exercises table β
ββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Trigger: notify_exercise_changeβ
β β
β β’ Captures exercise_id β
β β’ Identifies operation type β
β β’ Sends NOTIFY event β
ββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Trigger Listener β
β (Always running) β
β β
β β’ Receives NOTIFY β
β β’ Parses payload β
β β’ Routes to handler β
ββββββββ¬ββββββββββββββββββββββββββββ
β
βββ INSERT/UPDATE
β β
β βΌ
β βββββββββββββββββββββββββββ
β β Exercise Sync β
β β β
β β 1. Fetch from DB β
β β 2. Build embedding text β
β β 3. Generate embedding β
β β 4. Upsert to Qdrant β
β βββββββββββββββββββββββββββ
β
βββ DELETE
β
βΌ
βββββββββββββββββββββββββββ
β Delete from Qdrant β
β β
β β’ Remove vector point β
β β’ Maintain consistency β
βββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ
β PostgreSQL: INSERT on β
β past_sessions table β
ββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Trigger: notify_past_session β
β _change β
β β
β β’ Captures session details β
β β’ Sends NOTIFY event β
ββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Workout Sync β
β β
β 1. Fetch session exercises β
β 2. Build combined embedding β
β 3. Generate embedding β
β 4. Store in Qdrant with β
β user_id and session_id β
ββββββββββββββββββββββββββββββββββββ
This background process ensures that:
- Any exercise added/updated in PostgreSQL is automatically synced to Qdrant
- Any completed workout session is automatically embedded and stored in Qdrant
- The vector database stays in sync with the relational database
- No manual intervention is required
π Internal Processes
Profile Data Extraction
The system uses a structured approach to extract profile information from conversational text:
- AI Response Format: Gemini is instructed to output profile data in a structured format:
<PROFILE_DATA>
{
age: 28,
weight: 75,
height: 180,
gender: "MALE",
goals: "build muscle",
injuries: "none",
lifestyle: "active",
equipment: "gym access"
}
</PROFILE_DATA>
- Extraction Logic (
utils.ts::extractProfileData):
- Searches for
<PROFILE_DATA>tags in AI response - Attempts TOON decoding first (compact format)
- Falls back to JSON parsing
- Validates each field individually
- Returns partial profile if incomplete
- Validation (
utils.ts::isProfileComplete):
- Checks all required fields: age, weight, height, gender, goals, injuries
- Optional fields: lifestyle, equipment
- Returns boolean for workflow decision
Vector Embedding Generation
Purpose: Convert text to 768-dimensional vectors for similarity search
Process:
- Exercise Embeddings (
utils.ts::buildEmbeddingText):
// Combines multiple fields const embeddingText = [ exercise.title, // "Squats" exercise.description, // "A compound lower body..." exercise.body_parts, // "legs, glutes, core" exercise.dif_level, // "MEDIUM" exercise.common_mistakes, // "Knees caving inward..." exercise.position, // "STANDING" exercise.steps, // "1. Stand with feet..." exercise.tips, // "Keep knees aligned..." ].join('\n'); // Send to Google's text-embedding-004 const embedding = await embedText(embeddingText); // Returns: [0.123, -0.456, 0.789, ...] (768 numbers)
- Workout Session Embeddings (
workoutSync.ts::buildWorkoutEmbeddingText):
// Fetches all exercises in a completed workout // Builds embeddings for each (without common_mistakes) // Concatenates all exercise texts // Generates single embedding representing entire workout
- Profile Query Embeddings (
workflow.ts::processExerciseRecommendation):
// Sends profile to AI for refinement // AI optimizes search query based on goals/injuries // Embeds refined query // Uses for similarity search
TOON Format Encoding
Why TOON? More compact than JSON, reducing token usage in AI prompts
Example:
// Original data const data = { ex1: { exerciseId: 'squat_001', reps: 12, duration: null }, ex2: { exerciseId: 'pushup_002', reps: 10, duration: null }, }; // JSON: 124 characters JSON.stringify(data); // TOON: ~80 characters (35% smaller) toonEncode(data);
Usage in Project:
- Encoding profile data for AI prompts
- Encoding exercise lists for recommendations
- Encoding past session data for context
- Decoding AI responses with structured data
Injury-Based Exercise Filtering
Objective: Prevent recommending exercises that could aggravate injuries
Implementation:
- Query Refinement (
utils.ts::refineSearchQueryWithGemini):
// Input: "Age: 28, Goals: build muscle, Injuries: knee pain" const response = await gemini.sendMessage({ message: `User profile: ${profileText} Injuries: ${injuries} Create refined search query and identify body parts to exclude.` }); // AI returns: { refinedQuery: "upper body strength exercises, core stability", excludeBodyParts: ["knees", "legs", "lower body"] }
- Qdrant Filtering (
workflow.ts::processExerciseRecommendation):
const searchOptions = { vector: embedding, limit: 50, filter: { must_not: [ { key: 'bodyParts', match: { any: ['knees', 'legs', 'lower body'], }, }, ], }, }; // Only returns exercises that DON'T target excluded body parts const results = await qdrant.search(EXERCISES_COLLECTION_NAME, searchOptions);
Session State Management
Storage: Redis with 24-hour TTL
Session Structure:
interface SessionState { userId: string; step: WorkflowStep; // Current stage conversationHistory: Message[]; // All messages profileData?: IUserProfile; // After collection exerciseRecommendations?: IExercise[]; // After generation selectedExercises?: IExercise[]; // After confirmation createdAt: string; updatedAt: string; }
Operations:
- Get Session (
session.ts::getSession):
const sessionKey = `session:${sessionId}`; const data = await redisClient.get(sessionKey); return data ? JSON.parse(data) : null;
- Set Session (
session.ts::setSession):
const sessionKey = `session:${sessionId}`; await redisClient.setEx( sessionKey, 86400, // 24 hours JSON.stringify(session) );
- Update Session (
session.ts::updateSession):
// Partial update - merges with existing const existingSession = await getSession(sessionId); const updatedSession = { ...existingSession, ...updates }; await setSession(sessionId, updatedSession);
- Auto-Cleanup: Redis automatically deletes sessions after 24 hours of inactivity
Retry Logic
Purpose: Handle transient failures in external services
Implementation (utils.ts::withRetry):
await withRetry( async () => { // Operation that might fail return await qdrantClient.upsert(...); }, { maxRetries: 3, // Try 3 times delayMs: 1000, // Wait 1s, 2s, 3s between attempts operationName: 'sync exercise to Qdrant' } );
Used for:
- Qdrant upsert operations
- Embedding generation
- Database queries in sync operations
PostgreSQL Triggers
Purpose: Automatically sync data changes to Qdrant without manual intervention
Exercise Trigger (schema.sql):
-- Function CREATE OR REPLACE FUNCTION notify_exercise_change() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify( 'exercise_changes', json_build_object( 'exercise_id', NEW.id, 'operation', TG_OP )::text ); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers (INSERT, UPDATE, DELETE) CREATE TRIGGER exercise_insert_trigger AFTER INSERT ON exercises FOR EACH ROW EXECUTE FUNCTION notify_exercise_change();
Workflow Session Trigger:
CREATE TRIGGER past_session_insert_trigger AFTER INSERT ON past_sessions FOR EACH ROW EXECUTE FUNCTION notify_past_session_change();
Listener (triggerListener.ts):
// Dedicated PostgreSQL connection for LISTEN client.query('LISTEN exercise_changes'); client.query('LISTEN past_session_changes'); client.on('notification', async msg => { if (msg.channel === 'exercise_changes') { const { exercise_id, operation } = JSON.parse(msg.payload); if (operation === 'DELETE') { await deleteExerciseFromQdrant(exercise_id); } else { await syncExerciseToQdrant(exercise_id); } } });
π Data Flow
Exercise Data Flow
ββββββββββββββββββββ
β Admin adds β
β new exercise β
β to PostgreSQL β
ββββββββββ¬ββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β exercises Table β
β β’ id (UUID) β
β β’ external_id (unique) β
β β’ title, description, body_parts β
β β’ difficulty, position, steps, tips β
β β’ thumbnail_URL, video_URL β
β β’ male_thumbnail_URL, male_video_URL β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Trigger: exercise_insert_trigger β
β β’ Fires on INSERT β
β β’ Sends pg_notify() β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Listener: handleExerciseNotification() β
β β’ Parses notification β
β β’ Calls syncExerciseToQdrant() β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β exerciseSync.ts β
β 1. Fetch exercise from DB β
β 2. Build embedding text: β
β title + description + body_parts + β
β difficulty + mistakes + position + β
β steps + tips β
β 3. Generate 768-dim embedding β
β 4. Parse body_parts & steps to arrays β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Qdrant: exercises Collection β
β β’ Point ID: exercise UUID β
β β’ Vector: [0.123, -0.456, ...] (768) β
β β’ Payload: β
β - external_id β
β - title β
β - bodyParts: ["legs", "glutes"] β
β - description β
β - difLevel: "MEDIUM" β
β - commonMistakes β
β - position: "STANDING" β
β - steps: ["Stand...", "Lower..."] β
β - tips β
β - timestamps β
βββββββββββββββββββββββββββββββββββββββββββββ
β
β (Later: Vector search)
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Workflow: Exercise Recommendation β
β β’ Search by profile embedding β
β β’ Filter by injury exclusions β
β β’ Return top 50 matches β
βββββββββββββββββββββββββββββββββββββββββββββ
Workout Session Data Flow
ββββββββββββββββββββ
β User completes β
β workout β
ββββββββββ¬ββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β POST /api/workflow β
β β’ userId, sessionId β
β β’ Exercise results JSON β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β processExerciseSummary() β
β 1. Generate AI summary β
β 2. Begin transaction β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL Transaction β
β β
β INSERT past_sessions β
β session_id: "session_abc" β
β user_id: "user_123" β
β date: NOW() β
β notes: AI summary β
β accuracy_score: 88.5 β
β efficiency_score: 82.3 β
β completion_percentage: 85.0 β
β calories_burned: 250.5 β
β ... (other metrics) β
β RETURNING id β session_db_id β
β β
β INSERT session_exercises (for each) β
β session_id: session_db_id β
β exercise_id: "squat_001" β
β order_index: 0 β
β β
β INSERT session_exercise_results (each) β
β session_id: session_db_id β
β exercise_id: "squat_001" β
β exercise_title: "Squats" β
β time_spent: 300 β
β repeats: 3 β
β total_reps: 36 β
β calories: 80.2 β
β mistakes: JSONB β
β average_accuracy: 0.92 β
β order_index: 0 β
β β
β COMMIT β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
β (Trigger fires)
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Trigger: past_session_insert_trigger β
β β’ Fires on INSERT to past_sessions β
β β’ Sends pg_notify() with: β
β - session_id (DB UUID) β
β - user_id β
β - original_session_id (session_abc) β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Listener: handlePastSessionNotification()β
β β’ Parses notification β
β β’ Calls syncWorkoutSessionToQdrant() β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β workoutSync.ts β
β 1. Query session exercises from DB: β
β SELECT e.* FROM exercises e β
β INNER JOIN session_exercise_results β
β WHERE session_id = ? β
β ORDER BY order_index β
β β
β 2. Build embedding text for each: β
β title + description + body_parts + β
β difficulty + position + steps + tips β
β (Note: NO common_mistakes) β
β β
β 3. Concatenate all exercise texts β
β 4. Generate single 768-dim embedding β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Qdrant: workout_sessions Collection β
β β’ Point ID: session DB UUID β
β β’ Vector: [0.789, 0.234, ...] (768) β
β β’ Payload: β
β - user_id: "user_123" β
β - session_id: "session_abc" β
ββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
β (Later: Next workout request)
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Exercise Recommendation Flow β
β β’ Embed new profile query β
β β’ Search workout_sessions β
β β’ Filter: user_id = "user_123" β
β β’ Find similar past workouts β
β β’ Use performance data for better recs β
βββββββββββββββββββββββββββββββββββββββββββββ
π Setup & Installation
Prerequisites
- Node.js 22+ and Yarn
- Docker & Docker Compose
- PostgreSQL 18 (via Docker)
- Qdrant (via Docker)
- Redis (via Docker)
- Google Cloud account (for Gemini API)
Local Development
- Clone the repository
git clone <repository-url> cd ailab_hack_backend
- Install dependencies
- Create environment file
- Configure environment variables
PORT=3000 NODE_ENV=development # Database DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ailab_hack # Qdrant QDRANT_URL=http://localhost:6333 # Redis REDIS_URL=redis://localhost:6379 # Google Gemini API GEMINI_API_KEY=your_gemini_api_key_here
- Start infrastructure services
# Start PostgreSQL, Qdrant, and Redis docker-compose up -d # Check services are running docker-compose ps
- Initialize database schema
# Connect to PostgreSQL psql postgresql://postgres:postgres@localhost:5432/ailab_hack # Run schema \i schema.sql # (Optional) Insert sample exercises \i insert_exercises.sql
- Start development server
The server will start on http://localhost:3000
Docker Development
Run everything in Docker:
# Start all services including the app docker-compose -f docker-compose.yml up -d # View logs docker-compose logs -f # Stop all services docker-compose down
Testing the API
# Health check curl http://localhost:3000/health # Start a conversation curl -X POST http://localhost:3000/api/workflow \ -H "Content-Type: application/json" \ -d '{ "userId": "test_user_1", "sessionId": "test_session_1", "messages": [ { "role": "user", "content": "I want to start a workout program" } ] }'
π Deployment
Production Setup
See DEPLOYMENT.md for detailed production deployment instructions.
Quick overview:
- Provision server (Ubuntu 22.04+)
- Install dependencies (Node.js, Docker, Nginx)
- Clone repository to
/var/www/ailab_hack_backend - Configure environment (
.env.production) - Set up SSL with Certbot
- Configure Nginx as reverse proxy
- Start with PM2 for process management
Production Architecture
Internet
β
βΌ
βββββββββββββββββββ
β Nginx β Port 443 (HTTPS)
β SSL Terminationβ
β Reverse Proxy β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β PM2 Process β Port 3000
β Manager β
β (Node.js App) β
ββββββββββ¬βββββββββ
β
ββββββ΄βββββ¬ββββββββββ¬ββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββ ββββββββββ ββββββββββ ββββββββββ
β Postgresβ β Qdrant β β Redis β β Gemini β
β Docker β β Docker β β Docker β β API β
βββββββββββ ββββββββββ ββββββββββ ββββββββββ
Environment Variables (Production)
NODE_ENV=production PORT=3000 DATABASE_URL=postgresql://user:pass@localhost:5432/ailab_hack QDRANT_URL=http://localhost:6333 REDIS_URL=redis://localhost:6379 GEMINI_API_KEY=your_production_key
Monitoring & Logs
# PM2 logs pm2 logs ailab-hack # Docker logs docker-compose -f docker-compose.prod.yml logs -f # Nginx logs tail -f /var/log/nginx/access.log tail -f /var/log/nginx/error.log # Database logs docker-compose -f docker-compose.prod.yml logs postgres
π API Documentation
For detailed API documentation including all endpoints, request/response formats, and examples, see:
Quick Reference
| Endpoint | Method | Purpose |
|---|---|---|
/health |
GET | Health check |
/api/workflow |
POST | Main conversation & workflow endpoint |
Workflow Steps:
PROFILE_INTAKE- Collect user informationPROFILE_CONFIRMATION- Confirm profile dataEXERCISE_RECOMMENDATION- Generate workoutEXERCISE_CONFIRMATION- Confirm exercisesEXERCISE_SUMMARY- Submit workout resultsCOMPLETED- Workflow finished
ποΈ Database Schema
Tables
exercises
- id: UUID (primary key) - external_id: VARCHAR (unique, used for integrations) - title: VARCHAR - description: TEXT - body_parts: TEXT (comma-separated) - dif_level: ENUM ('EASY', 'MEDIUM', 'HARD') - common_mistakes: TEXT - position: ENUM ('STANDING', 'SEATED', 'FLOOR') - steps: TEXT (newline-separated) - tips: TEXT - thumbnail_URL: TEXT - video_URL: TEXT - male_thumbnail_URL: TEXT - male_video_URL: TEXT - created_at: TIMESTAMP - updated_at: TIMESTAMP
past_sessions
- id: UUID (primary key) - session_id: VARCHAR (unique, from client) - user_id: VARCHAR (indexed) - date: TIMESTAMP (indexed) - notes: TEXT (includes AI summary) - target_duration_seconds: INTEGER - completed_reps_count: INTEGER - target_reps_count: INTEGER - calories_burned: DECIMAL(10, 2) - completion_percentage: DECIMAL(5, 2) - total_mistakes: INTEGER - accuracy_score: DECIMAL(5, 2) - efficiency_score: DECIMAL(5, 2) - total_exercise: INTEGER - actual_hold_time_seconds: INTEGER - target_hold_time_seconds: INTEGER - created_at: TIMESTAMP - updated_at: TIMESTAMP
session_exercises
- id: UUID (primary key) - session_id: UUID (FK β past_sessions.id) - exercise_id: VARCHAR (FK β exercises.external_id) - order_index: INTEGER - UNIQUE(session_id, order_index)
session_exercise_results
- id: UUID (primary key) - session_id: UUID (FK β past_sessions.id) - exercise_id: VARCHAR (FK β exercises.external_id) - exercise_title: VARCHAR - time_spent: INTEGER (seconds) - repeats: INTEGER (sets completed) - total_reps: INTEGER - total_duration: INTEGER (for timer-based) - calories: DECIMAL(10, 2) - average_accuracy: DECIMAL(3, 2) - mistakes: JSONB (array of mistake objects) - order_index: INTEGER - UNIQUE(session_id, order_index)
Qdrant Collections
exercises
Vector Size: 768 dimensions
Distance: Cosine similarity
Payload:
- external_id (string)
- title (string)
- bodyParts (array of strings)
- description (string)
- difLevel (string)
- commonMistakes (string)
- position (string)
- steps (array of strings)
- tips (string)
- createdAt (ISO string)
- updatedAt (ISO string)
workout_sessions
Vector Size: 768 dimensions
Distance: Cosine similarity
Payload:
- user_id (string, indexed)
- session_id (string)
Redis Keys
session:{sessionId}
Value: JSON string of SessionState
TTL: 86400 seconds (24 hours)