- Python 50.6%
- HTML 49.2%
- Dockerfile 0.2%
- Points system based on MET values from the Compendium of Physical Activities, with HR-based intensity multiplier and elevation bonus. - Trophy/achievement system with 36 milestones across 10 categories. - Direct messaging between users with HTMX-powered chat and unread notification badges. - Public user profiles linkable from leaderboards and chats. - Strava sport_type mapping expanded to cover all 47 Strava types (was 12), plus --force-update and --prune-stale sync flags. - REST API for HealthKit / Health Connect ingestion via Bearer token, with per-user token management in the profile UI. - Per-challenge chat threads. - Email/password registration alongside PocketID SSO. - Hourly Strava sync added to the scheduler. - Challenge editing for creators (every field, with leaderboard recalc). - Family invite codes switched to name-based slugs. - Charts (line, donut, stacked bar) for family activity comparison. - 404/403/500 branded error pages. - ADMIN_EMAILS env var replaces hardcoded admin promotion. |
||
|---|---|---|
| accounts | ||
| activities | ||
| challenges | ||
| dm | ||
| families | ||
| static | ||
| templates | ||
| trophies | ||
| workoutharder | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| backup_db.py | ||
| blog-post.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| manage.py | ||
| README.md | ||
| requirements.txt | ||
| scheduler.py | ||
Workout Harder
A self-hosted family fitness challenge app. Create challenges, compete on shared leaderboards, and track each other's progress. Activities sync automatically from Strava.
Workout Harder. Be Kinder.
Features
- Strava sync — OAuth connect, real-time webhook for new activities, periodic backfill
- PocketID SSO — single sign-on via your own self-hosted OIDC provider
- Challenges — pick a metric (distance / duration / elevation / activity count / calories), an activity type, a date range, and an optional goal target. Share via invite code or link.
- Family groups — shared space for a household. Everyone in a group is auto-added to any challenge a member creates.
- Live leaderboards per challenge and per family
- Imperial / metric toggle per user — each family member sees their own units regardless of others' preferences
- Weekly digest emails — every Sunday at noon (local TZ), each member gets their family's recap with leaderboard, top performer, and challenge progress
- Automatic DB backups — gzipped SQLite snapshots on every container start and daily at 3 AM, kept 30 days
Stack
- Django 5.1 + Gunicorn
- SQLite (bind-mounted, easy to back up / migrate to Postgres later)
- Tailwind CSS via CDN, HTMX for dynamic bits
- WhiteNoise for static files
- Pure-stdlib scheduler container for cron-like tasks
Quick start
1. Configure
git clone https://github.com/YOUR_USER/workoutharder.git
cd workoutharder
cp .env.example .env
# edit .env with your secrets
You'll need:
- A Strava API app — https://www.strava.com/settings/api
- A PocketID instance with an OIDC client registered (or any OIDC provider — adjust accounts/views.py endpoints if needed)
- SMTP credentials for outbound email
Set the OIDC redirect URI in your PocketID admin to:
https://YOUR_HOST/accounts/pocketid/callback/
Set the Strava callback URL to:
https://YOUR_HOST/accounts/strava/callback/
2. Run
docker compose up -d --build
The web app listens on port 4598. Reverse-proxy it behind your TLS terminator.
3. Sign in
Visit https://YOUR_HOST/ and sign in with PocketID. Then connect Strava from the profile page.
Operating it
Manual digest send
docker exec workoutharder python manage.py send_weekly_digests --dry-run # preview
docker exec workoutharder python manage.py send_weekly_digests # send for real
Backups
docker exec workoutharder python manage.py backup_db # create one now
docker exec workoutharder python manage.py restore_db --list # list snapshots
docker exec workoutharder python manage.py restore_db latest # restore most recent
docker compose restart web # apply restore
Backups live in ./backups/ on the host as db_YYYYMMDD_HHMMSS.sqlite3.gz.
Strava sync
docker exec workoutharder python manage.py sync_strava --days 30
The Strava webhook posts to /activities/webhook/strava/. Register your webhook subscription with verify_token = first 16 chars of your SECRET_KEY.
Data model
accounts.UserProfile— extendsauth.Userwith Strava tokens, PocketID sub, unit preference, and stats cachefamilies.Family+FamilyMembership— group + role join tablechallenges.Challenge+ChallengeParticipant— challenge config + per-user progressactivities.Activity— synced Strava activities
Project layout
.
├── accounts/ # auth, profile, PocketID, Strava OAuth, unit conversion tags
├── activities/ # Strava sync, webhook, management commands (backup, digest, sync)
├── challenges/ # challenge CRUD, leaderboard recalculation
├── families/ # groups, invite links, auto-add to challenges
├── workoutharder/ # Django project (settings, urls, wsgi)
├── templates/ # all UI templates (Tailwind via CDN)
├── backup_db.py # standalone SQLite backup script
├── scheduler.py # cron-like loop: daily backup + weekly digest
└── docker-compose.yml
License
MIT