9.2 KiB
9.2 KiB
Architecture
Analysis Date: 2026-02-15
Pattern Overview
Overall: Monolithic multi-tier architecture with separated frontend and backend services.
Key Characteristics:
- Frontend: Single-Page Application (SPA) with React + React Router
- Backend: Express.js REST API with direct database queries
- Database: PostgreSQL with relational schema
- State Management: React Context API for authentication, local component state for page-level data
- Communication: HTTP/JSON via Fetch API with Bearer token authentication
- Deployment: Containerized (Docker) with Traefik reverse proxy routing
Layers
Presentation Layer (Frontend):
- Purpose: Render UI, handle user interactions, manage local state and navigation
- Location:
/workspace/gravl/frontend/src/ - Contains: React pages, components, context providers, CSS styling
- Depends on: React, React Router, AuthContext, backend API endpoints
- Used by: Browser clients
Application/Page Layer (Frontend):
- Purpose: Manage view logic, fetch data, orchestrate navigation between different views
- Location:
/workspace/gravl/frontend/src/pages/ - Contains: Full page components (Dashboard, WorkoutPage, ProfilePage, ProgressPage, LoginPage, RegisterPage, OnboardingWizard, WorkoutSelectPage)
- Depends on: AuthContext, Icon components, API calls via fetch
- Used by: App.jsx routing logic
Context Layer (Frontend):
- Purpose: Provide global authentication state and user session management
- Location:
/workspace/gravl/frontend/src/context/AuthContext.jsx - Contains: Auth state, login/register/logout functions, token management, localStorage integration
- Depends on: React hooks, backend authentication endpoints
- Used by: All protected pages and components
API/REST Layer (Backend):
- Purpose: Handle HTTP requests, validate input, manage authentication, route requests to data layer
- Location:
/workspace/gravl/backend/src/index.js - Contains: Express routes for auth, user profile, programs, exercises, logs, progression
- Depends on: PostgreSQL connection, JWT verification, bcrypt password hashing
- Used by: Frontend via HTTP requests
Data Layer (Backend):
- Purpose: Execute queries against PostgreSQL database
- Location: Database queries within
/workspace/gravl/backend/src/index.jsusing pg Pool - Contains: User management, program/day/exercise definitions, workout logs, measurements, strength records
- Depends on: PostgreSQL driver (pg)
- Used by: API layer for all data operations
Database Layer:
- Purpose: Persist application data
- Location:
/workspace/gravl/db/init.sql(schema definition) - Contains: 7 tables (users, programs, program_days, exercises, program_exercises, workout_logs, user_measurements, user_strength)
- Depends on: PostgreSQL engine
- Used by: Backend data layer
Data Flow
User Registration/Login Flow:
- User enters credentials on RegisterPage or LoginPage
- Page calls
useAuth().register()oruseAuth().login()from AuthContext - AuthContext makes POST to
/api/auth/registeror/api/auth/login - Backend validates credentials (register: email uniqueness + hash password; login: password verification)
- Backend returns JWT token and user object
- AuthContext stores token in localStorage and sets user state
- Navigation redirects to
/onboarding(incomplete) or/(complete)
Onboarding Flow:
- User completes OnboardingWizard with profile data (gender, age, experience, goal, measurements, strength)
- Wizard calls
useAuth().updateProfile()with profile data - Backend updates users table and related measurement/strength tables
- Sets
onboarding_complete = true - User navigated to Dashboard
Workout/Exercise Flow:
- Dashboard displays program days and selected day's workout
- User clicks workout day,
onStartWorkout()called - App.jsx calls
fetchProgram()to load program with all days/exercises - App.jsx calls
fetchLogs()to fetch existing workout logs for that day - WorkoutPage displayed with exercises and weight/rep input fields
- User enters weight/reps and clicks "Log Set"
logSet()calls POST/api/logswith exercise_id, weight, reps, date, set_number- Backend checks if log exists for that set (update) or creates new (insert)
- Response updates local logs state
- WorkoutPage re-renders with updated data
Progression Calculation Flow:
- WorkoutPage calls
fetchProgression()for each exercise - Backend fetches last workout for that exercise (last 10 logs, completed only)
- Analyzes if all sets hit max_reps
- Returns suggestedWeight (same weight or +2.5kg if maxed out)
- Frontend displays suggestion in workout interface
Profile/Measurements Flow:
- User navigates to ProfilePage
- Page calls parallel fetches:
/api/user/profile,/api/user/measurements,/api/user/strength - Backend joins latest measurements and strength records with user profile
- Page displays current profile and can add new measurements or strength records
- User saves changes → updates user profile state in AuthContext
State Management:
- Global state: User session, authentication token (AuthContext in localStorage)
- Page-level state: Program, logs, current view, selected day (App.jsx state)
- Component-level state: Form inputs, editing mode, expanded sections (individual page components)
- No shared state management library: Direct React Context + local useState
Key Abstractions
AuthContext:
- Purpose: Centralized authentication and user session management
- Examples:
useAuth()hook returns { user, token, loading, register, login, logout, updateProfile, refreshProfile } - Pattern: React Context + custom hook for easy access from any component
Page Components:
- Purpose: Encapsulate view logic, form handling, and local data fetching
- Examples:
Dashboard.jsx,WorkoutPage.jsx,ProfilePage.jsx - Pattern: Functional components with useState/useEffect, direct API calls via fetch
Program/Exercise Model:
- Purpose: Represent training structure in database and API
- Structure: Program > Days > Exercises (program_exercises join table) > Logs (user workout records)
- Pattern: Nested JSON responses from
/api/programs/:idendpoint
Workout Log:
- Purpose: Record individual set performance (weight, reps, completion status)
- Examples:
workout_logstable with user_id, program_exercise_id, date, set_number, weight, reps, completed - Pattern: Upsert logic (update if exists, insert if new)
Entry Points
Frontend Entry:
- Location:
frontend/index.html→src/main.jsx→src/App.jsx - Triggers: Browser loads gravl.homelab.local
- Responsibilities:
- Bootstrap React app with BrowserRouter and AuthProvider
- Define route structure (auth routes vs. protected routes)
- Initialize token from localStorage and verify session
- Render main App component
Backend Entry:
- Location:
backend/src/index.js - Triggers: Docker container startup (
npm start→node src/index.js) - Responsibilities:
- Initialize Express app and PostgreSQL connection pool
- Mount CORS and JSON middleware
- Define all API routes with request/response handling
- Listen on port 3001
- Database queries executed inline within route handlers
Auth-Protected Routes:
- ProtectedRoute wrapper checks user existence and onboarding status
- Redirects to
/loginif unauthenticated - Redirects to
/onboardingif authenticated but onboarding incomplete - Routes:
/,/profile,/progress,/select-workout,/workout
Auth Routes:
- AuthRoute wrapper redirects to
/or/onboardingif already authenticated - Routes:
/login,/register
Error Handling
Strategy: Try-catch in Express routes returns JSON errors; frontend logs errors and may show error UI
Patterns:
- Backend: Catch database/auth errors, return 400/401/500 with JSON error message
- Frontend: Catch fetch errors in async functions, log to console, optionally show in component error state
- Validation: Frontend form validation (required, minLength); backend re-validates email uniqueness
- Auth failures: Return 401 Unauthorized, AuthContext logs user out
- Database errors: Return 500 with generic message (details in server logs only)
Cross-Cutting Concerns
Logging:
- Backend:
console.error()for exceptions; logs visible in Docker container stdout - Frontend:
console.error()for network failures and state issues
Validation:
- Frontend: HTML5 form validation (type="email", minLength, required)
- Backend: Email lowercase normalization, null coercion for numeric fields, JWT signature verification
Authentication:
- Method: JWT Bearer token in Authorization header
- Token storage: localStorage on browser
- Token validation:
authMiddlewarefunction checks header and verifies signature - Token lifetime: 30 days expiration
- Session management: AuthContext refresh on mount, logout clears localStorage and state
CORS:
- Enabled globally with
cors()middleware on all routes - Frontend proxy configured in Vite for
/apicalls during development - Docker network configured for service-to-service communication
Data Integrity:
- Foreign key constraints in database schema (ON DELETE CASCADE)
- Unique email constraint in users table
- Indexes on frequently queried columns (user_id, date, program_exercise_id)
Architecture analysis: 2026-02-15