Project Dashboard
COSMO Workout Builder — full-stack serverless fitness app on AWS
Tech Stack
cosmo.pablobhz.cloudlambda_shared). API Gateway HTTP API at api.pablobhz.cloudKey URLs
| Service | URL | Purpose |
|---|---|---|
| Frontend | https://cosmo.pablobhz.cloud | React SPA (Amplify) |
| Public API | https://api.pablobhz.cloud/* | Main API Gateway (up8szzocti) |
| Admin API | https://api.pablobhz.cloud/admin/* | Admin API Gateway (rnxx0jo6fg) |
| CDN | https://d15bqj3wrouqr4.cloudfront.net | Exercise images (CloudFront) |
| Auth | cosmo-auth.auth.us-east-1.amazoncognito.com | Cognito domain |
System Architecture
How all pieces connect end-to-end
Terraform Module Graph
Lambda Handlers
26 compute + 5 auth + 1 asset = 32 total Lambda functions
| Handler | Function Name | Routes | Tables Used | Timeout |
|---|---|---|---|---|
unified_exercises_handler.py | cosmo-exercises | GET/PUT/POST/DELETE /exercises | cosmo-exercises | 30s |
workouts_handler.py | cosmo-workouts | GET/POST/DELETE /workouts | cosmo-workouts, cosmo-users | 30s |
sessions_handler.py | cosmo-sessions | GET/POST/DELETE /sessions | cosmo-workout-sessions, cosmo-users, cosmo-plateau-alerts | 30s |
session_draft_handler.py | cosmo-session-drafts | GET/PUT /session-drafts | cosmo-session-drafts | 15s |
measurements_handler.py | cosmo-measurements | GET/POST/DELETE /measurements | cosmo-measurements | 30s |
profile_handler.py | cosmo-profile | GET/PUT /profile | cosmo-users | 15s |
calendar_handler.py | cosmo-calendar | GET/POST/DELETE /calendar | cosmo-calendar | 30s |
achievements_handler.py | cosmo-achievements | GET/POST/PUT /achievements | cosmo-achievements | 30s |
community_workouts_handler.py | cosmo-community-workouts | GET/POST/DELETE /community-workouts | cosmo-community-workouts | 30s |
community_exercises_handler.py | cosmo-community-exercises | GET/POST/PUT/DELETE /community-exercises | cosmo-community-exercises | 30s |
messages_handler.py | cosmo-messages | GET/POST /messages, PUT /read, GET /unread, GET/POST /board, GET/POST /board/accept, GET /users/search | cosmo-messages, cosmo-users | 15s |
trainer_links_handler.py | cosmo-trainer-links | GET/POST/PUT/DELETE /trainer-links | cosmo-trainer-links | 30s |
plateau_alerts_handler.py | cosmo-plateau-alerts | GET/POST/PUT/DELETE /plateau-alerts | cosmo-plateau-alerts | 30s |
feedback_handler.py | cosmo-feedback | POST /feedback, GET/PUT /admin/feedback | cosmo-feedback | 30s |
user_layout_handler.py | cosmo-user-layout | GET/PUT /user-layout | cosmo-user-layout | 15s |
user_media_handler.py | cosmo-user-media | POST /user-media/upload-url, DELETE /user-media/{type} | cosmo-users + S3 | 30s |
share_handler.py | cosmo-share | POST/GET /share | cosmo-workouts | 30s |
payments_handler.py | cosmo-payments | POST /payments/create-checkout, POST /webhook, POST /cancel, GET /status | cosmo-users, cosmo-subscriptions + Stripe | 15s |
tier_handler.py | cosmo-tier | GET /tier, POST /tier/check-feature | cosmo-users, cosmo-tier-config | 15s |
analytics_handler.py | cosmo-analytics | POST /analytics/event, GET /analytics/summary | cosmo-analytics-events, cosmo-users | 10s |
app_config_handler.py | cosmo-app-config | GET/PUT /app-config, GET/PUT /app-config/i18n, GET /app-config/founder | cosmo-app-config, cosmo-tier-config | 15s |
gym_hub_handler.py | cosmo-gym-hub | GET /gyms, GET /gyms/{gymId}/members | cosmo-users | 30s |
gym_reviews_handler.py | cosmo-gym-reviews | GET/POST /gym-reviews | cosmo-gym-reviews | 30s |
guest_workouts_handler.py | cosmo-guest-workouts | POST/GET /guest-workouts | cosmo-workouts-guest | 30s |
pdf_export_handler.py | cosmo-pdf-export | POST /export-pdf | — (reportlab layer) | 60s / 512MB |
admin_trainer_handler.py | cosmo-admin-trainer | GET/PUT /admin/trainer-requests | cosmo-users | 30s |
| Handler | Function Name | Routes / Trigger | Tables Used |
|---|---|---|---|
admin_users_handler.py | cosmo-admin-users | GET /users, PUT /users/{id}/* | cosmo-users + Cognito |
admin_dashboard_handler.py | cosmo-admin-analytics | GET /analytics | cosmo-exercises + Cognito |
admin_presigned_url_handler.py | cosmo-admin-presigned-url | POST /presigned-url | S3 (cosmo-exercise-assets) |
api_docs_handler.py | cosmo-api-docs | GET /api-docs | API Gateway (route discovery) |
post_auth_handler.py | cosmo-post-auth | Cognito PostAuthentication trigger | cosmo-users, cosmo-tier-config |
S3-triggered Lambda for media processing. Defined in root-level terraform/gif_to_webp.tf.
| Handler | Function Name | Trigger | Tables Used | Layer |
|---|---|---|---|---|
gif_to_webp_handler.py | cosmo-gif-to-webp | S3 PutObject on exercises/**/*.gif | cosmo-exercises | Pillow (PIL) |
Flow: Admin uploads GIF → S3 event → Lambda downloads GIF → converts to WebP via Pillow → uploads .webp back to S3 → updates gifUrl in DynamoDB to CDN WebP URL.
API Routes
All endpoints on both API Gateways
| Method | Route | Handler | Auth |
|---|---|---|---|
| Exercises | |||
| GET | /exercises | unified_exercises_handler | JWT |
| POST | /exercises | unified_exercises_handler | JWT (admin) |
| PUT | /exercises | unified_exercises_handler | JWT (admin) |
| DELETE | /exercises | unified_exercises_handler | JWT (admin) |
| Workouts | |||
| GET | /workouts | workouts_handler | JWT |
| POST | /workouts | workouts_handler | JWT |
| DELETE | /workouts | workouts_handler | JWT |
| Sessions | |||
| GET | /sessions | sessions_handler | JWT |
| POST | /sessions | sessions_handler | JWT |
| DELETE | /sessions | sessions_handler | JWT |
| Profile | |||
| GET | /profile | profile_handler | JWT |
| PUT | /profile | profile_handler | JWT |
| Messages & DMs | |||
| GET | /messages | messages_handler | JWT |
| POST | /messages | messages_handler | JWT |
| PUT | /messages/read | messages_handler | JWT |
| GET | /messages/unread | messages_handler | JWT |
| GET | /messages/board | messages_handler | JWT |
| POST | /messages/board | messages_handler | JWT |
| GET | /messages/board/accept | messages_handler | JWT |
| POST | /messages/board/accept | messages_handler | JWT |
| GET | /messages/users/search | messages_handler | JWT |
| Payments | |||
| POST | /payments/create-checkout | payments_handler | JWT |
| POST | /payments/webhook | payments_handler | Public (Stripe sig) |
| POST | /payments/cancel | payments_handler | JWT |
| GET | /payments/status | payments_handler | JWT |
| Measurements | |||
| GET | /measurements | measurements_handler | JWT |
| POST | /measurements | measurements_handler | JWT |
| DELETE | /measurements | measurements_handler | JWT |
| Calendar | |||
| GET | /calendar | calendar_handler | JWT |
| POST | /calendar | calendar_handler | JWT |
| DELETE | /calendar | calendar_handler | JWT |
| Community | |||
| GET | /community-workouts | community_workouts_handler | JWT |
| POST | /community-workouts | community_workouts_handler | JWT |
| DELETE | /community-workouts | community_workouts_handler | JWT |
| GET | /community-exercises | community_exercises_handler | JWT |
| POST | /community-exercises | community_exercises_handler | JWT |
| PUT | /community-exercises | community_exercises_handler | JWT |
| DELETE | /community-exercises | community_exercises_handler | JWT |
| Session Drafts | |||
| GET | /session-drafts | session_draft_handler | JWT |
| PUT | /session-drafts | session_draft_handler | JWT |
| Guest Workouts | |||
| POST | /guest-workouts | guest_workouts_handler | Public |
| GET | /guest-workouts | guest_workouts_handler | Public |
| Other | |||
| GET/PUT | /user-layout | user_layout_handler | JWT |
| GET/POST/PUT/DELETE | /achievements | achievements_handler | JWT |
| GET/POST/PUT/DELETE | /trainer-links | trainer_links_handler | JWT |
| GET/POST/PUT/DELETE | /plateau-alerts | plateau_alerts_handler | JWT |
| GET/POST | /share | share_handler | JWT / Public |
| GET | /tier | tier_handler | JWT |
| POST | /tier/check-feature | tier_handler | JWT |
| GET | /gyms | gym_hub_handler | JWT |
| GET/POST | /gym-reviews | gym_reviews_handler | JWT / Public |
| POST | /feedback | feedback_handler | Public |
| POST | /analytics/event | analytics_handler | JWT |
| GET/PUT | /app-config | app_config_handler | JWT |
| GET | /app-config/founder | app_config_handler | JWT |
| POST | /user-media/upload-url | user_media_handler | JWT |
| POST | /export-pdf | pdf_export_handler | JWT |
All admin routes require JWT from admin Cognito client. Accessed via api.pablobhz.cloud/admin/* (prefix stripped by domain mapping).
| Method | Route (as seen by Lambda) | Handler |
|---|---|---|
| GET | /users | admin_users_handler |
| PUT | /users/{id}/admin | admin_users_handler |
| PUT | /users/{id}/confirm | admin_users_handler |
| PUT | /users/{id}/disable | admin_users_handler |
| PUT | /users/{id}/enable | admin_users_handler |
| PUT | /users/{id}/tier | admin_users_handler |
| PUT | /users/{id}/lifetime | admin_users_handler |
| GET | /analytics | admin_dashboard_handler |
| POST | /presigned-url | admin_presigned_url_handler |
| GET | /api-docs | api_docs_handler |
| GET | /feedback | feedback_handler (R25) |
| PUT | /feedback | feedback_handler (R25) |
DynamoDB Tables
21 tables, all PAY_PER_REQUEST billing
| Table | Key Design | GSIs | TTL | Used By |
|---|---|---|---|---|
cosmo-exercises | pk/sk | by-muscle-group | — | exercises, admin |
cosmo-workouts | pk=USER#, sk=WORKOUT#|TRASH#|SHARE# | — | — | workouts, share |
cosmo-workout-sessions | pk=USER#, sk=SESSION#ts#id | by-workout | — | sessions |
cosmo-users | userId | GSI_GYM | — | profile, payments, tier, gym, messages |
cosmo-measurements | pk=USER#, sk=MEASUREMENT#date#id | — | — | measurements |
cosmo-calendar | pk=USER#, sk=DATE# | by-month | — | calendar |
cosmo-achievements | pk=USER#, sk=BADGE# | — | — | achievements |
cosmo-community-workouts | pk=COMMUNITY, sk=WORKOUT# | by-author, by-category | — | community_workouts |
cosmo-community-exercises | pk=AUTHOR#, sk=EXERCISE# | by-muscle | — | community_exercises |
cosmo-messages | pk=CONV#|USER#|BOARD#, sk=MSG#|CONV# | — | ttl (90d board) | messages |
cosmo-trainer-links | pk=TRAINER#, sk=STUDENT# | by-student | — | trainer_links |
cosmo-plateau-alerts | pk=USER#, sk=PLATEAU# | — | — | plateau_alerts, sessions |
cosmo-feedback | pk=FEEDBACK#, sk=META | by-date, by-context | — | feedback |
cosmo-user-layout | pk=userId, sk=LAYOUT | — | — | user_layout |
cosmo-subscriptions | pk=userId, sk=subscriptionId | — | — | payments |
cosmo-tier-config | pk=CONFIG, sk=TIERS | — | — | tier, app_config, post_auth |
cosmo-workouts-guest | pk=GUEST#, sk=WORKOUT# | — | 24h | guest_workouts |
cosmo-gym-reviews | pk=GYM#, sk=REVIEW# | — | — | gym_reviews |
cosmo-analytics-events | pk=USER#, sk=EVENT#ts#id | by-event-name | ttl | analytics |
cosmo-app-config | pk=OPTIONS|I18N#, sk=type | — | — | app_config |
cosmo-session-drafts | (per-user) | — | — | session_draft |
Pages & Router
43 pages across 4 route groups
Wrapped in AppLayout (desktop sidebar + mobile bottom nav)
| Route | Component | API Endpoints Used |
|---|---|---|
/dashboard | UserDashboard | GET /sessions, /achievements, /plateau-alerts |
/builder | WorkoutBuilder | GET /exercises, POST /workouts |
/session | WorkoutSession | POST /sessions, PUT /session-drafts |
/session-v2 | WorkoutSessionV2Page | POST /sessions (V2 session with stack/list modes, 3-state SetButton, inline editing) |
/workouts | MyWorkouts | GET/DELETE /workouts |
/profile | ProfilePage | GET/PUT /profile, POST /user-media/upload-url |
/calendar | CalendarPage | GET/POST/DELETE /calendar |
/community | CommunityPage | GET/POST /community-workouts, /community-exercises |
/messages | MessagesPage | GET/POST /messages, /board, /users/search |
/achievements | AchievementsPage | GET /achievements |
/gyms | GymsPage | GET /gyms, /gym-reviews |
/coach | CoachDashboard | GET/POST/DELETE /trainer-links |
/settings | SettingsPage | GET/PUT /user-layout, PUT /profile (workoutViewMode) |
/upgrade | UpgradePage | POST /payments/create-checkout, /cancel, GET /status |
/statistics | StatisticsPage | GET /sessions?stats=true |
/onboarding | OnboardingPage | PUT /profile (initial setup) |
Wrapped in PublicLayout (Navbar + Footer)
| Route | Component |
|---|---|
/ | LandingPage |
/contact | ContactPage |
/about | AboutPage |
/faq | FAQPage |
/terms | TermsPage |
/terms-gate | TermsGatePage |
/shared/:shareId | SharedWorkout (public view) |
/maintenance | MaintenancePage (env: VITE_MAINTENANCE_MODE, bypasses /admin routes) |
| Route | Component |
|---|---|
/login | LoginPage |
/signup | SignupPage |
/forgot-password | ForgotPasswordPage |
/auth/callback | AuthCallback (Google OAuth) |
Wrapped in AdminLayout. Accessed via /admin/*
| Route | Component | Admin API Endpoint |
|---|---|---|
/admin | AdminDashboard | GET /analytics |
/admin/users | UserManagement | GET /users, PUT /users/{id}/* |
/admin/exercises | ExerciseList | GET /exercises (main API) |
/admin/exercises/:id | ExerciseEditor | PUT /exercises (main API) |
/admin/dedup | ExerciseDedup | GET /exercises |
/admin/options | AdminOptionsPage | GET/PUT /app-config |
/admin/translations | AdminTranslationsPage | GET/PUT /app-config/i18n |
/admin/feedback | FeedbackPanel | GET/PUT /feedback |
/admin/api-docs | ApiDocs | GET /api-docs |
/admin/login | AdminLogin | — |
Components
60+ reusable components organized by domain
API Client Modules
24 frontend API modules — all import from apiClient.js
| Module | Backend Lambda | DynamoDB Table |
|---|---|---|
api/auth.js | Cognito (Amplify SDK) | — |
api/exercises.js | unified_exercises_handler | cosmo-exercises |
api/workouts.js | workouts_handler | cosmo-workouts |
api/sessions.js | sessions_handler | cosmo-workout-sessions |
api/profile.js | profile_handler | cosmo-users |
api/measurements.js | measurements_handler | cosmo-measurements |
api/calendar.js | calendar_handler | cosmo-calendar |
api/achievements.js | achievements_handler | cosmo-achievements |
api/messages.js | messages_handler | cosmo-messages |
api/communityWorkouts.js | community_workouts_handler | cosmo-community-workouts |
api/communityExercises.js | community_exercises_handler | cosmo-community-exercises |
api/trainerLinks.js | trainer_links_handler | cosmo-trainer-links |
api/share.js | share_handler | cosmo-workouts |
api/tier.js | tier_handler | cosmo-users, cosmo-tier-config |
api/feedback.js | feedback_handler | cosmo-feedback |
api/gyms.js | gym_hub_handler | cosmo-users |
api/gymReviews.js | gym_reviews_handler | cosmo-gym-reviews |
api/userLayout.js | user_layout_handler | cosmo-user-layout |
api/userMedia.js | user_media_handler | cosmo-users + S3 |
api/plateauAlerts.js | plateau_alerts_handler | cosmo-plateau-alerts |
api/pdfExport.js | pdf_export_handler | — |
api/analytics.js | analytics_handler | cosmo-analytics-events |
api/adminApi.js | admin_users, admin_analytics | cosmo-users + Cognito |
api/adminTrainer.js | admin_trainer_handler | cosmo-users |
api/adminUpload.js | admin_presigned_url_handler | S3 (presigned URL + direct upload) |
Hooks & Utilities
Custom React hooks and helper modules
Hooks (7)
| Hook | Purpose |
|---|---|
useFormState.js | Form state management with dirty tracking |
useMediaQuery.js | Responsive breakpoint detection |
useUserStats.js | Aggregated user statistics (sessions, PRs, streaks) |
usePlusMenu.js | Plus button menu options (user-customizable) |
useTierGate.js | Feature gating by subscription tier |
useAppConfig.js | Dynamic app config from API (equipment, categories, muscles, difficulty, hubs). Cached in sessionStorage with 5min TTL, falls back to appDefaults.js |
useUnreadMessages.js | Polls GET /messages/unread every 30s for sidebar badge |
Utilities (12)
| File | Purpose |
|---|---|
apiClient.js | Centralized HTTP client with typed errors (ApiError), auth headers, 401 handling |
authUtils.js | getCurrentUser, clearStaleTokens, profile extraction |
profileCache.js | localStorage cache for user profile (avoids redundant API calls) |
storageHelpers.js | Safe localStorage get/set with JSON parse/stringify |
formatters.js | Date, number, unit formatting (kg/lbs, duration, etc.) |
workoutUtils.js | Workout-specific helpers (volume calc, exercise grouping) |
shareHelper.js | Share link generation and clipboard copy |
errorHandler.js | Consistent error handling with toast notifications |
confirmDialog.js | Programmatic confirmation dialogs |
loadingState.js | Loading/error/data state management |
analytics.js | Event tracking (POST /analytics/event) |
handleSaveError.jsx | Error boundary for save operations |
Terraform Modules
6 modules, ~80 .tf files
| Module | Files | Purpose | Key Resources |
|---|---|---|---|
| foundation | 3 | Base infrastructure | Route53 zone, tags, naming |
| shared | 1 + python/ | Lambda shared layer | aws_lambda_layer_version (lambda_shared) |
| data | 22 | DynamoDB tables | 21 aws_dynamodb_table resources + outputs |
| auth | 13 | Cognito + Admin API | User pool, OAuth, admin API Gateway, 5 admin Lambdas |
| compute | 36 | Main API + Lambdas | API Gateway, 26 Lambdas, SSL cert, custom domain, IAM |
| frontend | 2 | Amplify hosting | aws_amplify_app, domain association |
Root-level Terraform (non-module)
| File | Purpose | Key Resources |
|---|---|---|
gif_to_webp.tf | GIF→WebP auto-conversion | IAM role, Pillow Lambda layer, Lambda function, S3 event notification trigger |
terraform_assets.tf | S3 + CloudFront for exercises | aws_s3_bucket (exercise_assets, user_media), aws_cloudfront_distribution |
Lambda Layers
| Layer | Purpose | Used By |
|---|---|---|
lambda_shared | response, auth, dynamo, serializers | All compute + auth Lambdas |
reportlab | PDF generation library | pdf_export_handler only |
stripe | Stripe Python SDK | payments_handler only |
pillow | PIL image processing (GIF→WebP) | gif_to_webp_handler only |
Auth & Admin API
Cognito setup and the admin API Gateway
Hosting & CDN
Amplify, S3, CloudFront
main branch. Builds with Vite, serves from frontend/dist. SPA catch-all routes to /index.html.Flow: Authentication
End-to-end auth from login to API call
Flow: Payments (Stripe)
Checkout, subscription management, cancellation
Flow: Messages
DMs and Community Board
Flow: Workout Session V2
R12-001: Complete live workout experience with 3-state set tracking
canEdit = (isActive && !fieldsLocked) || set.done
│
├─ On "Finish Workout":
│ ├─ Collects all stats → builds payload
│ └─ POST /sessions (existing sessions_handler.py)
│ └─ Payload: workoutId, workoutName, startedAt, finishedAt, durationSeconds, exercises[{sets[]}]
│
└─ SessionSummary.jsx — post-workout stats
├─ Total duration, volume per exercise (sets × reps × kg)
├─ Rest analysis (planned vs actual)
├─ Skipped/modified sets highlighted
└─ Premium CTA at bottom
Flow: Media Pipeline
R19-003 & R19-010: Exercise image upload, auto-conversion, and gallery
exercises/**/*.gif
│ ├─ Triggers gif_to_webp_handler.py Lambda
│ ├─ Pillow converts GIF → WebP (animated preserved)
│ ├─ Uploads .webp to same S3 path
│ └─ Updates DynamoDB gifUrl to CDN WebP URL
│
└─ Gallery UI (R19-010):
├─ ExerciseEditor: thumbnail grid, hover to promote (★) or remove (✕)
├─ ExerciseCard: GifModal carousel with prev/next arrows + dot navigation
└─ "Ver animação" button shows +N count when gallery exists
Changelog (R12 — R19)
Recent feature additions and improvements
| Commit | Feature | Files Changed | Details |
|---|---|---|---|
| R12-001 | Workout Session V2 | 7 new components + 1 new page + profile_handler + SettingsPage + App.jsx | Complete live workout experience: 3-state SetButton (ready/active/rest), stack & list modes, inline-editable cells, session summary. Route: /session-v2. Profile field: workoutViewMode |
| R19-001 | Dynamic Hub Management | appDefaults.js, ExerciseEditor.jsx | Added HUB_OPTIONS constant (11 curated exercise hubs). ExerciseEditor gets hub badge UI with inline hub creation |
| R19-002 | Maintenance Mode | MaintenancePage.jsx, App.jsx, .env, .env.production | Env var VITE_MAINTENANCE_MODE gates all non-admin routes. Full-page maintenance screen with bilingual text |
| R19-003 | GIF→WebP Auto-Conversion | gif_to_webp.tf, gif_to_webp_handler.py, rezip_lambdas.sh | S3-triggered Lambda converts uploaded GIFs to WebP via Pillow. Pillow Lambda layer. Updates exercise gifUrl in DynamoDB |
| R19-004/005 | Admin Filter Persistence + Dismiss Duplicates | ExerciseList.jsx, ExerciseEditor.jsx | ExerciseList filters/sort persisted to localStorage. ExerciseEditor duplicate detection now dismissable with sorted key pairs |
| R19-006/007 | Fix Hub “undefined” Bug | ExerciseEditor.jsx | Removed all hub.emoji references causing “undefined” display after HUB_OPTIONS schema change |
| R19-008/009 | Hub CRUD + Dynamic Options | AdminOptionsPage.jsx, useAppConfig.js, ExerciseEditor.jsx | AdminOptionsPage gets HubSection (full CRUD: add/rename/remove hubs). useAppConfig now fetches HUBS from API. ExerciseEditor switched from static imports to useAppConfig() for dynamic equipment/categories/muscles/hubs |
| R19-010 | Multi-Image Gallery | ExerciseEditor.jsx, ExerciseCard.jsx | Gallery upload section in ExerciseEditor (max 10 images, promote-to-main, remove). GifModal in ExerciseCard replaced with carousel (arrows, dots, counter). +N gallery hint on “Ver animação” button |
Environment Variables Introduced
| Variable | Commit | Purpose |
|---|---|---|
VITE_MAINTENANCE_MODE | R19-002 | Boolean flag to show maintenance page for all non-admin routes |
Schema Changes
| Location | Commit | Change |
|---|---|---|
cosmo-users (profile) | R12-001 | Added workoutViewMode: "stack" | "list" (default: stack) |
cosmo-app-config | R19-008 | Added HUBS: { items: [{ key, label }] } option type |
cosmo-exercises (media) | R19-010 | Added media.gallery: string[] (up to 10 CDN URLs) |