A full-stack workout tracking app with training cycle management, session logging, exercise progression, data analytics, and Fitbit integration.
Production: https://workout-production-cb80.up.railway.app
| Layer | Tech |
|---|---|
| Frontend | Next.js 16 (App Router), TypeScript, Tailwind CSS, shadcn/ui, Recharts |
| Backend | FastAPI, SQLAlchemy (async), asyncpg |
| Database | PostgreSQL (Railway) / SQLite (local dev) |
| Auth | JWT via httpOnly cookies, bcrypt, rate limiting |
| Deployment | Docker (single container), nginx, supervisord, Railway |
| Integrations | Fitbit OAuth2 |
- Training cycles — Meso/micro cycle planning with goal tracking (Strength, Hypertrophy, Endurance)
- Sessions — Create, log, and complete workouts with real-time set tracking, rest timers, and a live workout timer
- Session analytics — Volume/session-count chart (8W / 6M / 1Y), stats strip, muscle group suggestions, month navigator
- Exercise history — Per-exercise progression view with set-by-set breakdown
- Plans — Reusable workout templates organised by weeks (Week 1 / Week 2 / ...) with draft-based editing — nothing commits to the database until you hit Save; bodyweight exercises hide weight fields throughout
- Exercises — Global exercise library with muscle group and Weighted / Bodyweight category; inline edit dialog from the exercises page
- AI Suggestions — RP-style weight algorithm using top-set reference, RPE-calibrated hypertrophy thresholds, and session-over-session progression; shown inline per exercise during sessions with one-tap apply/undo; meso-cycle-aware; every suggestion is logged per-user in
suggestion_logs - Fitbit — Sync steps, heart rate, sleep, and weight; today's stats on dashboard and sessions page
- Auth — Secure JWT login/register with rate limiting and OWASP-compliant password rules
- Data import — Bulk import workout history from the Strong app via CSV (
seed_all.py) - Mobile-first UI — Fixed bottom navigation (Dashboard · Sessions · + · Exercises · More), full-screen session experience with slide-up transition, scroll-driven title fade, swipe-to-delete sets, swipe-to-replace exercises
browser
└── nginx (port $PORT, Railway)
├── /api/* → FastAPI :8000
└── /* → Next.js :3000
└── /api/* rewrites → FastAPI :8000 (local dev only)
Single Docker container runs three processes via supervisord:
- nginx — listens on
$PORT(injected at startup viaenvsubst) - FastAPI (uvicorn) — port 8000
- Next.js — port 3000
Login and register set an httpOnly JWT cookie. All protected API routes validate the cookie via the get_current_user_id dependency (backend/app/deps.py).
| Property | Implementation |
|---|---|
| Password storage | bcrypt via passlib |
| Session token | JWT HS256, 24h expiry |
| Token transport | httpOnly; SameSite=lax; Secure cookie |
| XSS protection | httpOnly prevents JS access to token |
| CSRF protection | SameSite=lax |
| Brute force | 5 attempts / 15 min per IP |
| Password policy | 8+ chars, uppercase, lowercase, digit (enforced in UI) |
POST /api/auth/registeror/api/auth/login— validates credentials, setsaccess_tokencookie- Browser sends cookie automatically on every request (
withCredentials: true) - FastAPI's
get_current_user_iddep reads and verifies the JWT POST /api/auth/logoutclears the cookie
- Python 3.10+
- Node.js 18+
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in values
uvicorn app.main:app --reload --port 8000API docs at http://localhost:8000/api/docs
cd frontend
npm install
npm run devApp at http://localhost:3000. API calls proxied to http://localhost:8000 via Next.js rewrites.
Backend (.env):
DATABASE_URL=postgresql://user:pass@localhost:5432/workout
JWT_SECRET_KEY=your-long-random-secret
FITBIT_CLIENT_ID=
FITBIT_CLIENT_SECRET=
FITBIT_REDIRECT_URI=http://localhost:3000/settings/fitbit/callback
ALLOWED_ORIGINS=http://localhost:3000Frontend (.env.local):
# Leave empty — Next.js rewrites proxy /api/ to the backend
NEXT_PUBLIC_API_URL=DATABASE_URL=postgresql://... # from Railway Postgres addon
JWT_SECRET_KEY= # openssl rand -hex 32
FITBIT_CLIENT_ID=
FITBIT_CLIENT_SECRET=
FITBIT_REDIRECT_URI=https://your-app.up.railway.app/settings/fitbit/callback
ALLOWED_ORIGINS=https://your-app.up.railway.app
RAILWAY_ENVIRONMENT=production # enables Secure flag on cookiesSet the deploy branch to prod in Railway service settings.
All endpoints except /api/auth/register, /api/auth/login, and /api/health require authentication (JWT cookie).
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/register |
Register — sets JWT cookie |
| POST | /api/auth/login |
Login — sets JWT cookie (rate limited) |
| POST | /api/auth/logout |
Clear JWT cookie |
| GET | /api/auth/me |
Get current user profile |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/exercises |
List exercises (?muscle_group= filter) |
| GET | /api/exercises/{id} |
Exercise details |
| GET | /api/exercises/{id}/history |
Exercise history for current user |
| POST | /api/exercises |
Create exercise |
| PUT | /api/exercises/{id} |
Update exercise |
| DELETE | /api/exercises/{id} |
Delete exercise |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/meso-cycles |
List user's cycles |
| POST | /api/meso-cycles |
Create cycle |
| PUT | /api/meso-cycles/{id} |
Update cycle |
| DELETE | /api/meso-cycles/{id} |
Delete cycle |
| GET | /api/meso-cycles/{id}/micro-cycles |
List micro cycles |
| POST | /api/meso-cycles/{id}/micro-cycles |
Create micro cycle |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sessions |
List user's sessions |
| GET | /api/sessions/{id} |
Session with exercises and sets |
| POST | /api/sessions |
Create session |
| PUT | /api/sessions/{id} |
Update session |
| DELETE | /api/sessions/{id} |
Delete session |
| POST | /api/sessions/{id}/start |
Start session |
| POST | /api/sessions/{id}/complete |
Complete (calculates volume) |
| POST | /api/sessions/{id}/cancel |
Cancel |
| POST | /api/sessions/{id}/exercises |
Add exercise |
| PUT | /api/sessions/session-exercises/{id} |
Update session exercise |
| DELETE | /api/sessions/session-exercises/{id} |
Remove exercise |
| POST | /api/sessions/session-exercises/{id}/sets |
Add set |
| PUT | /api/sessions/exercise-sets/{id} |
Update set |
| DELETE | /api/sessions/exercise-sets/{id} |
Delete set |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/plans |
List user's plans |
| GET | /api/plans/{id} |
Plan with sessions |
| POST | /api/plans |
Create plan |
| DELETE | /api/plans/{id} |
Delete plan |
| GET | /api/plans/templates |
Built-in templates (PPL, Upper/Lower, Full Body) |
| POST | /api/plans/{id}/sessions |
Add session to plan |
| PUT | /api/plans/plan-sessions/{id} |
Update plan session |
| DELETE | /api/plans/plan-sessions/{id} |
Delete plan session |
| POST | /api/plans/plan-sessions/{id}/exercises |
Add exercise |
| PUT | /api/plans/plan-exercises/{id} |
Update plan exercise |
| DELETE | /api/plans/plan-exercises/{id} |
Delete plan exercise |
| GET | /api/plans/plan-sessions/{id}/preview |
Preview with suggested weights |
| POST | /api/plans/plan-sessions/{id}/apply |
Apply to a training session |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/suggestions/exercises |
Exercise suggestions by volume |
| GET | /api/suggestions/weight |
RP-style weight suggestion (?exercise_id=, optional ?meso_cycle_id=); auto-logs result |
| GET | /api/suggestions/weight/history |
Past suggestions per user (?exercise_id=, ?meso_cycle_id=, ?limit=) |
| PATCH | /api/suggestions/weight/history/{log_id} |
Record actual weight/reps/RPE against a suggestion |
| GET | /api/suggestions/muscle-groups |
Volume by muscle group |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/fitbit/auth-url |
Get Fitbit OAuth2 URL |
| POST | /api/fitbit/callback |
Exchange code for tokens |
| GET | /api/fitbit/status |
Connection status |
| GET | /api/fitbit/today-stats |
Steps, HR, weight, sleep for today |
| POST | /api/fitbit/disconnect |
Clear tokens |
| POST | /api/fitbit/sync-session/{id} |
Sync HR + health metrics for a session |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check |
users — credentials + fitbit tokens
exercises — global exercise library
meso_cycles — 4-12 week training blocks
micro_cycles — weekly breakdowns
training_sessions — individual workouts
session_exercises — exercises within a session
exercise_sets — sets per exercise (reps, weight, RPE)
health_metrics — Fitbit sleep/weight data per session
volume_history — calculated volume per exercise per session
plans — reusable workout templates
plan_sessions — sessions within a plan (grouped by week_number)
plan_exercises — exercises within a plan session
suggestion_logs — per-user weight suggestion history with optional outcome tracking
workout/
├── Dockerfile
├── nginx.conf
├── supervisord.conf
├── start.sh
├── backend/
│ ├── app/
│ │ ├── api/
│ │ │ ├── auth.py # JWT login/register/logout/me
│ │ │ ├── exercises.py
│ │ │ ├── meso_cycles.py
│ │ │ ├── sessions.py
│ │ │ ├── plans.py
│ │ │ ├── suggestions.py
│ │ │ └── fitbit.py
│ │ ├── models/models.py # SQLAlchemy ORM models
│ │ ├── schemas/schemas.py # Pydantic DTOs
│ │ ├── services/
│ │ │ └── fitbit_service.py # Fitbit API client + auto token refresh
│ │ ├── deps.py # get_current_user_id JWT dependency
│ │ ├── database.py
│ │ └── main.py
│ └── requirements.txt
└── frontend/
└── src/
├── app/
│ ├── page.tsx # Dashboard
│ ├── login/page.tsx # Login page
│ ├── register/page.tsx # Register page
│ ├── cycles/
│ ├── sessions/
│ ├── plans/
│ ├── suggestions/
│ ├── exercises/
│ └── settings/
├── components/
│ ├── app-shell.tsx # Layout + auth guard wrapper
│ ├── auth/
│ │ └── auth-guard.tsx # Redirects unauthenticated users to /login
│ ├── shared/
│ │ └── navigation.tsx # Nav with logged-in user + logout
│ └── ui/ # shadcn components
├── contexts/
│ └── auth-context.tsx # Auth state, login/register/logout
└── lib/
└── api.ts # Axios with withCredentials, 401 handler
- Register app at dev.fitbit.com
- Callback URL:
http://localhost:3000/settings/fitbit/callback - Scopes:
activity heartrate profile sleep weight
- Callback URL:
- Set
FITBIT_CLIENT_ID,FITBIT_CLIENT_SECRET,FITBIT_REDIRECT_URIin backend.env - Go to Settings → Connect Fitbit
Tokens are automatically refreshed when within 5 minutes of expiry.
seed_all.py is a one-shot script that:
- Finds (or creates) the user by
SEED_EMAIL - Seeds all powerbuilding exercises
- Creates the full 12-week Powerbuilding Phase 2 plan (48 sessions)
- Imports a Strong app CSV export as completed training sessions
cd backend
source venv/bin/activate
# Seed local SQLite
python seed_all.py
# Seed Railway production
DATABASE_URL="postgresql+asyncpg://user:pass@host/db" \
SEED_EMAIL="you@example.com" \
python seed_all.pyThe script is idempotent — re-running skips already-existing sessions and plans.
Export from the Strong app (Settings → Export Data) and place the file at backend/strong_workouts.csv. Columns used: Date, Workout Name, Exercise Name, Set Order, Weight, Reps, RPE.
The project has a full test suite for both the backend API and frontend UI. All tests run automatically on every push and pull request via GitHub Actions.
105 integration tests using pytest + pytest-asyncio against an in-memory SQLite database. Each test gets a fresh schema — no shared state between tests.
cd backend
pip install -r requirements.txt
pytest tests/ -v| File | Coverage |
|---|---|
tests/test_auth.py |
Register, login, logout, /me, rate limiting (429) |
tests/test_sessions.py |
Full lifecycle: create → start → log sets → complete/cancel, volume calculation, PR detection, pre-summary |
tests/test_exercises.py |
CRUD, muscle group filter, history (warmup exclusion, user isolation, limit) |
tests/test_suggestions.py |
All RPE thresholds, top-set logic, rounding, meso filter, suggestion logs, outcome recording |
tests/test_meso_cycles.py |
CRUD, micro cycles, cascade delete |
42 tests using Jest + React Testing Library.
cd frontend
npm test # run once
npm run test:watch # interactive / TDD mode| File | Coverage |
|---|---|
src/__tests__/auth-context.test.tsx |
Loading state, login/register/logout flows, error propagation, useAuth guard |
src/__tests__/login-page.test.tsx |
Form rendering, password toggle, loading state, error messages (401, 429, generic), re-enable after failure |
src/__tests__/suggestion-algorithm.test.ts |
Pure TypeScript mirror of the RP weight algorithm — all RPE thresholds, rounding edge cases, no-RPE fallback, multi-session progression |
GitHub Actions runs both suites on every push and PR (.github/workflows/ci.yml). A Docker build check runs additionally on master and prod branches, gated on both test jobs passing.
MIT