Files
gravl/.planning/codebase/ARCHITECTURE.md
T
2026-02-15 21:49:31 +01:00

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.js using 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:

  1. User enters credentials on RegisterPage or LoginPage
  2. Page calls useAuth().register() or useAuth().login() from AuthContext
  3. AuthContext makes POST to /api/auth/register or /api/auth/login
  4. Backend validates credentials (register: email uniqueness + hash password; login: password verification)
  5. Backend returns JWT token and user object
  6. AuthContext stores token in localStorage and sets user state
  7. Navigation redirects to /onboarding (incomplete) or / (complete)

Onboarding Flow:

  1. User completes OnboardingWizard with profile data (gender, age, experience, goal, measurements, strength)
  2. Wizard calls useAuth().updateProfile() with profile data
  3. Backend updates users table and related measurement/strength tables
  4. Sets onboarding_complete = true
  5. User navigated to Dashboard

Workout/Exercise Flow:

  1. Dashboard displays program days and selected day's workout
  2. User clicks workout day, onStartWorkout() called
  3. App.jsx calls fetchProgram() to load program with all days/exercises
  4. App.jsx calls fetchLogs() to fetch existing workout logs for that day
  5. WorkoutPage displayed with exercises and weight/rep input fields
  6. User enters weight/reps and clicks "Log Set"
  7. logSet() calls POST /api/logs with exercise_id, weight, reps, date, set_number
  8. Backend checks if log exists for that set (update) or creates new (insert)
  9. Response updates local logs state
  10. WorkoutPage re-renders with updated data

Progression Calculation Flow:

  1. WorkoutPage calls fetchProgression() for each exercise
  2. Backend fetches last workout for that exercise (last 10 logs, completed only)
  3. Analyzes if all sets hit max_reps
  4. Returns suggestedWeight (same weight or +2.5kg if maxed out)
  5. Frontend displays suggestion in workout interface

Profile/Measurements Flow:

  1. User navigates to ProfilePage
  2. Page calls parallel fetches: /api/user/profile, /api/user/measurements, /api/user/strength
  3. Backend joins latest measurements and strength records with user profile
  4. Page displays current profile and can add new measurements or strength records
  5. 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/:id endpoint

Workout Log:

  • Purpose: Record individual set performance (weight, reps, completion status)
  • Examples: workout_logs table 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.htmlsrc/main.jsxsrc/App.jsx
  • Triggers: Browser loads gravl.homelab.local
  • Responsibilities:
    1. Bootstrap React app with BrowserRouter and AuthProvider
    2. Define route structure (auth routes vs. protected routes)
    3. Initialize token from localStorage and verify session
    4. Render main App component

Backend Entry:

  • Location: backend/src/index.js
  • Triggers: Docker container startup (npm startnode src/index.js)
  • Responsibilities:
    1. Initialize Express app and PostgreSQL connection pool
    2. Mount CORS and JSON middleware
    3. Define all API routes with request/response handling
    4. Listen on port 3001
    5. Database queries executed inline within route handlers

Auth-Protected Routes:

  • ProtectedRoute wrapper checks user existence and onboarding status
  • Redirects to /login if unauthenticated
  • Redirects to /onboarding if authenticated but onboarding incomplete
  • Routes: /, /profile, /progress, /select-workout, /workout

Auth Routes:

  • AuthRoute wrapper redirects to / or /onboarding if 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: authMiddleware function 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 /api calls 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