Self‑hosted strength‑training analytics for Strong app exports. Import your CSV, see PRs, volume trends, rep ranges, and workout history — all stored locally in SQLite.
See it in action: lifting.dakheera47.com (forgive the low numbers, I just started!)
git clone https://github.com/DaKheera47/strong-statistics.git
cd strong-statistics
cp .env.example .env # set INGEST_TOKEN to a long random string
docker compose up -dThen open:
- Dashboard → http://localhost:8069/
Edit .env before first run:
| Variable | Required | Default | What it does |
|---|---|---|---|
INGEST_TOKEN |
✅ | — | Secret required to upload CSVs via /ingest. |
Data is bind‑mounted to ./data and logs are stored in a named Docker volume by the included docker-compose.yml.
- Export from Strong (iOS/Android): Settings → Export Data → CSV.
- Upload to strong-statistics using your token.
cURL
curl -X POST "http://localhost:8069/ingest?token=$INGEST_TOKEN" \
-F "file=@/path/to/strong-export.csv"HTTPie
http -f POST :8069/ingest?token=$INGEST_TOKEN file@/path/to/strong-export.csvExpected response
{
"stored": "strong_20240914_123456.csv",
"rows": 1230
}Safe to re‑upload newer exports — duplicates are ignored.
Goal: export from the Strong app, the iOS share sheet pops up, you tap a shortcut, and it POSTs the CSV straight to your server.
-
Open Shortcuts on iOS → tap + to create a new shortcut.
-
Name it “Send to strong‑statistics”.
-
Tap the info (ⓘ) button → enable Show in Share Sheet → under Accepts, select Files (CSV).
-
Add action Get Contents of URL:
-
URL:
http://YOUR_DOMAIN:8069/ingest?token=<TOKEN>Replace
YOUR_DOMAINand<TOKEN>with your real domain and INGEST_TOKEN. -
Method:
POST -
Request Body:
Form -
Add form field: Name
file→ TypeFile→ Value Shortcut Input (a.k.a. Provided Input) -
(Optional) If you prefer header auth instead of query: set Headers →
X-Token: <your INGEST_TOKEN>and remove?token=...from the URL.
-
-
(Optional) Add Show Result to see the JSON response after upload.
If you don’t see the shortcut in the share sheet later, scroll to the bottom → Edit Actions → enable it.
- In Strong: Settings → Export Data.
- The share sheet opens automatically → select Send to strong‑statistics.
- Wait a moment; you’ll get a success response. Open your dashboard to see new data.
Tip: Large exports can take a few seconds; you can re‑run later — duplicates are skipped.
The dashboard provides several analytics widgets in collapsible sections:
- Recent Workouts - List of recent training sessions with details
- Progressive Overload Widget - Track strength progression over time
- Session Volume Trend - Visualize total training volume trends
- Volume Sparklines - Per-exercise volume trends in compact charts
- Max Weight Sparklines - Track maximum weights over time per exercise
- Rep Range Distribution - Analyze your preferred rep ranges
Navigation:
- Main Dashboard →
/(all analytics widgets) - All Workouts →
/workouts(detailed workout history table) - Click any workout row to view detailed sets and exercises
The application runs as three containers orchestrated by Docker Compose:
-
api(FastAPI backend) - Handles CSV ingestion, data processing, and serves favicon/health endpoints- Built from
Dockerfile.api(Python 3.12-slim) - Exposes port 8000 internally
- Health checks via
/healthendpoint - Data directory bind-mounted to
./data - Logs stored in named volume
strong_logs
- Built from
-
web(Next.js frontend) - Analytics dashboard and workout viewer- Built from
frontend/Dockerfile.web(Node.js 22-alpine with pnpm) - Exposes port 3000 internally
- Read-only access to SQLite database via bind-mounted
./data - Built with Turbopack for faster builds
- Built from
-
proxy(Caddy reverse proxy) - Routes traffic between frontend and backend- Routes
/ingest*and/healthto API backend - Routes everything else to Next.js frontend
- Single external port 8069 for all traffic
- Configured via
Caddyfile
- Routes
Data persistence:
- SQLite database and uploads:
./datadirectory (bind-mounted to host) - Application logs:
strong_logsnamed Docker volume
Backend (FastAPI):
GET /health→ Health check with last ingested timestampPOST /ingest?token=<TOKEN>→ Upload Strong CSV export (requires token)GET /favicon.svg→ App iconGET /favicon.ico→ App icon (fallback)GET /apple-touch-icon.png→ iOS home screen icon
Frontend API routes: (Next.js API routes)
GET /api/recent-workouts→ Recent workout listGET /api/recent-workouts?date=YYYY-MM-DD&workout_name=...→ Specific workout details
Frontend queries SQLite directly for analytics data via server-side API routes.
- Keep
INGEST_TOKENsecret. Don’t post it in screenshots.
From the repo root:
git pull
docker compose up -d --build- Can't reach dashboard → Make sure you're using port 8069 (not 8000)
- 401 on
/ingest→ Missing/incorrect?token=orX-Tokenheader - 400 on
/ingest→ Wrong form field (must befile) or not a Strong CSV export - 500 on
/ingest→ Check Docker logs:docker compose logs api - Database errors → SQLite database issues, check file permissions in
./data - Container startup issues → Run
docker compose up(without-d) to see logs - Frontend won't load → Check if all containers are healthy:
docker compose ps
MIT.
- Discord:
dakheera47 - Email: shaheer30sarfaraz@gmail.com
- Website: https://dakheera47.com



