A self-hosted, privacy-first workout tracker inspired by Strong. No accounts, no cloud, no subscriptions — just your data on your own server.
FastAPI · React PWA · SQLite · Docker Compose
- Start a workout, add exercises on the fly, log sets with weight + reps
- Previous session's sets shown inline as reference
- Reorder exercises during a workout
- Rename workouts, add notes
- Mark rest days from the home screen
- Configurable duration (60 / 90 / 120 / 180s) saved per profile
- Web Audio ding + vibration on finish
- Background-accurate — stays correct after screen lock or tab switch
- Push notification fires even when the screen is off (Android + iOS PWA)
- Full workout log with monthly calendar view
- Per-workout detail sheet — stats, muscle group breakdown, sets with estimated 1RM
- PRs per exercise (Epley 1RM), grouped by body part
- Weekly / daily volume bar chart (sets or tonnage)
- Muscle group donut chart
- 26-week activity heatmap
- Multiple profiles on a single instance
- Per-profile: unit (kg / lbs), theme, avatar colour, week start day, rest duration, ding toggle
- Themes: Dark, Light, AMOLED, Tokyo Night, Dracula, Nord, Gruvbox, Rosé Pine, Catppuccin Mocha / Macchiato / Frappé / Latte
- Strong CSV import — bring in your full workout history
- CSV export per profile
- Installable on iOS and Android
- Offline support via service worker
- Flamingo barbell icon, themed status bar
The service worker provides two things:
| Feature | How it works |
|---|---|
| Offline support | Static assets cached on first load; API falls back to cached responses when offline |
| Background notifications | Rest timer end time posted to SW on start; SW fires showNotification at the right time regardless of whether the page is suspended |
Platform support:
| Platform | Offline | Background notification |
|---|---|---|
| Android | ✅ | ✅ |
| iOS 16.4+ (PWA) | ✅ | ✅ — add to home screen first |
| iOS < 16.4 / desktop | ✅ | ❌ — in-page ding still works when visible |
| Layer | Tech |
|---|---|
| Backend | FastAPI + SQLModel + SQLite, Python 3.11 |
| Frontend | React 18 + Vite + plain CSS |
| Serving | nginx (frontend), uvicorn (backend) |
| Containers | Docker + Docker Compose |
| CI | GitHub Actions → ghcr.io (amd64 + arm64) |
./scripts/dev_up.shBuilds both containers and seeds the exercise library.
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8000 |
| API docs | http://localhost:8000/docs |
Reset the database:
rm data/lifty.db && docker compose restart backendImages are built and pushed to ghcr.io on every push to main (amd64 + arm64).
Create a docker-compose.yml on your host:
services:
backend:
image: ghcr.io/bndct-devops/lifty-backend:latest
restart: unless-stopped
expose:
- "8000"
volumes:
- /mnt/user/appdata/lifty:/data # adjust path as needed
environment:
- LIFTY_DB=/data/lifty.db
# - LIFTY_PASSWORD=your-password-here
frontend:
image: ghcr.io/bndct-devops/lifty-frontend:latest
restart: unless-stopped
ports:
- "3420:80"
depends_on:
- backendThen:
docker compose pull
docker compose up -dUpdate the volume path to wherever you want the SQLite database stored on your host.
By default the API has no authentication — fine for local/VPN use, but set a password before exposing to the internet.
Uncomment LIFTY_PASSWORD in your docker-compose.yml:
environment:
- LIFTY_DB=/data/lifty.db
- LIFTY_PASSWORD=your-strong-passwordRestart the backend. The app will show a password screen on load. Once unlocked, a 30-day JWT is stored in the browser — you won't be prompted again until it expires.
- Change/reset password: Settings → Instance Auth → Change Password (or set
LIFTY_PASSWORDenv var again and restart to override) - Sign out: Settings → Instance Auth → Sign Out
backend/
main.py # FastAPI app, all endpoints
models.py # SQLModel table definitions
schemas.py # Pydantic request/response types
db.py # engine + table creation
seed_exercises.py # built-in exercise library (runs on startup)
frontend/
public/
sw.js # service worker — offline cache + background notifications
manifest.json # PWA manifest
favicon.svg # flamingo barbell icon
src/
App.jsx # entire frontend
api.js # fetch wrappers for all backend endpoints
styles.css # CSS custom properties + layout
nginx.conf # proxies /api/* to backend
scripts/
dev_up.sh # one-command local dev start
docker-compose.yml # local dev (builds from source)
.github/workflows/
build-push.yml # CI: build multi-arch images, push to ghcr.io
- Inspired by Strong — the best commercial workout tracker, which lifty aims to self-host-replace
- Catppuccin theme palette by Catppuccin





