A full-stack calisthenics workout tracker — built for learning purposes.
Handcrafted in 2019 with inspiration from Flask Mega Tutorial, now enhanced with the assistance of AI.
Log your training sessions, define custom exercises with progression levels and workout templates, and connect with other athletes.
Originally a Flask/Jinja2 monolith, the project has evolved into a Flask REST API + Next.js web + Expo (React Native) mobile architecture with Supabase Auth.
| Layer | Technology |
|---|---|
| Backend | Python 3.14, Flask 3.1, SQLAlchemy 2.0, Flask-Migrate (Alembic) |
| Web frontend | Next.js 16 (App Router), React 19, TypeScript |
| Mobile frontend | Expo (React Native) SDK 57, Expo Router, TypeScript |
| Auth | Supabase Auth (ES256 JWTs, JWKS verification) |
| State Management | TanStack Query (React Query) 5 |
| Forms | react-hook-form 7 |
| Styling | Tailwind CSS 4 (web), NativeWind 4 (mobile) |
| Database | SQLite (dev) / PostgreSQL via Supabase (prod) |
| Production | Gunicorn + Next.js, nginx reverse proxy, systemd |
| Testing | pytest + pytest-cov (backend, 400 tests), Jest + React Native Testing Library (mobile) |
| Code Quality | mypy, black, flake8, isort, pre-commit |
- Log workouts with multiple exercises, sets, and rep counts or duration values
- Create and use workout templates — start a workout pre-filled from a template
- Smart workout form: exercise picker with category filters, progression level dropdowns, counting-type-aware inputs (reps vs mm:ss duration)
- Edit and delete workouts, toggle done/pendent status
- Create and manage custom exercise definitions with ordered progression levels (e.g. Tuck Planche → Straddle Planche → Full Planche)
- Categorize exercises and filter by category
- Browse and copy exercises from other athletes
- Counting type per exercise: reps or duration
- Create workout templates as reusable blueprints
- Define exercises and empty set placeholders
- Start a workout directly from a template
- Explore workouts from other athletes
- Follow / unfollow other users
- View user profiles with workout history
- Private messaging between users
- Notification badge for unread messages
- Registration with Supabase email confirmation
- Password reset via Supabase
- Edit profile (username, about me)
- Sign out
- Python 3.11+
- Node.js 20+
- PostgreSQL (optional — SQLite works for local dev)
cd backend
python3 -m venv .venv
source .venv/bin/activate
cd backend
pip install -r requirements.txtCreate backend/.env.local:
SECRET_KEY="a-hard-to-guess-secret-key"
SECURITY_PASSWORD_SALT="a-unique-salt-string"
FLASK_ENV="development"
# Supabase Auth
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_JWT_SECRET="your-supabase-jwt-secret"
# Database — SQLite is used automatically if DATABASE_URL is not set
# DATABASE_URL="postgresql://localhost/calisthenics_dev"
# Email (optional — for notifications)
# MAIL_SERVER="smtp.yourmailprovider.com"
# MAIL_PORT="587"
# MAIL_USE_TLS="1"
# MAIL_USERNAME="your-username"
# MAIL_PASSWORD="your-password"
# MAIL_DEFAULT_SENDER="noreply@yourdomain.com"Initialize the database and run:
flask db upgrade
flask run --port 5001cd web
npm installCreate web/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:5001/api/v1
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-keyRun the dev server:
npm run devThe app will be available at http://localhost:3000.
Requires Xcode (iOS Simulator) and/or Android Studio (Android emulator).
cd mobile
npm installCreate mobile/.env.local:
EXPO_PUBLIC_API_URL=http://localhost:5001/api/v1
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-keyThe app uses native modules not supported by Expo Go, so it needs a
development build rather than expo start alone:
npx expo run:ios # or: npx expo run:androidOnce the dev client is installed, day-to-day iteration can go back to
npx expo start --dev-client with fast refresh — only native-dependency
or config changes (app.json, new native packages) require re-running
expo run:ios/expo run:android.
Tests are configured in pytest.ini and run with coverage by default.
cd backend
source .venv/bin/activate
# Run all tests with coverage report
pytest
# Run only unit tests
pytest tests/unit/
# Run only integration tests
pytest tests/integration/cd mobile
npm testJest (jest-expo preset) + React Native Testing Library.
Pre-commit hooks enforce consistent style before each commit.
pre-commit install # install hooks
pre-commit run --all-files # run manually
mypy project/ # type check
black project/ tests/ # format
isort project/ tests/ # sort imports
flake8 project/ tests/ # lintThe web app runs on a Hetzner VPS (Ubuntu 24.04) behind nginx:
- Flask API — Gunicorn, systemd service, port 8000
- Next.js Frontend —
next start, systemd service, port 3002 - nginx — reverse proxy, SSL via Let's Encrypt
- Database — Supabase PostgreSQL
The mobile app is not distributed yet — build/distribution setup (EAS build profiles, TestFlight) is in progress.
I started this project in 2019 to learn how to build a real-world Python application from the ground up and to scratch my own itch as a calisthenics enthusiast. Over time, it turned into a playground for experimenting with better architecture, testing practices, and deployment setups. In its latest iteration, it also became a way to explore how modern AI assistants and autonomous coding agents can support day-to-day development work — from shaping features to keeping the codebase clean.
This project was built to gain hands-on experience with key patterns and tools found in real-world full-stack applications:
- Applied the application factory pattern to instantiate the app with separate configurations for development, testing, and production
- Organized features using Flask Blueprints for
auth,main,api, anderrorsmodules - Modeled a non-trivial schema with SQLAlchemy ORM, including many-to-many relationships (followers, exercise categories), cascading deletes, and relationship loading
- Managed schema migrations across environments using Flask-Migrate / Alembic without data loss
- Implemented Supabase Auth integration — ES256 JWT verification via JWKS, auto-provisioning Flask users from Supabase UUIDs
- Built a Next.js 16 App Router frontend with server/client component architecture
- Managed server state with TanStack Query — cache invalidation, optimistic updates, parameterized queries
- Built complex dynamic forms with react-hook-form —
useFieldArrayfor nested arrays,useWatchfor reactive field observation,setValuefor programmatic updates - Deployed a two-process production setup behind nginx with systemd, SSL, and PostgreSQL
- Tested the application using pytest-flask with 400 tests covering unit and integration scenarios
- Enforced code quality with type annotations, pre-commit hooks, and tools like
black,flake8, andisort - Built an Expo Router / React Native mobile app sharing the same Flask API and Supabase Auth as the web frontend — same
TanStack Query+react-hook-formpatterns as web, ported through platform-specific concerns like secure token storage (expo-secure-store, chunked around iOS Keychain's per-item size limit), deep-linked auth flows, and native-vs-web styling parity via NativeWind - Set up Jest + React Native Testing Library for the mobile app, including a real regression test for a shipped bug (an optimistic cache update that assumed a single data shape)
- Leveraged AI assistance (Claude and autonomous agents) to guide feature design, streamline development, and maintain high code quality