Personal health metrics platform powered by Garmin Connect.
Built to solve one problem: stepping on a scale and getting that weight into Garmin Connect should be as fast as tapping your phone on an NFC tag.
- Step on your scale, read your weight
- Tap your phone on an NFC sticker attached to the scale
- The VitalForge PWA opens instantly — type the number, hit Log
- Weight is pushed to Garmin Connect and saved locally in under a second
No opening apps, no navigating menus, no waiting for Bluetooth sync. Just weigh, tap, done.
From there, VitalForge grew into a full health dashboard that pulls all your Garmin data (sleep, HRV, resting HR, stress, body battery, VO2 max, training load) and surfaces trends and AI-powered recommendations.
- vitalforge-weight (port 8085) — Mobile-first PWA for quick weight logging to Garmin Connect
- vitalforge-dashboard (port 8086) — Health metrics dashboard with trends and AI-powered recommendations
No building required. Pull and run the latest images:
curl -O https://raw.githubusercontent.com/bearyjd/vitalforge/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/bearyjd/vitalforge/main/.env.example
cp .env.example .env
# Edit .env with your Garmin credentials and auth settings
docker compose -f docker-compose.prod.yml up -dImages are published to both registries on every push:
| Registry | Weight | Dashboard |
|---|---|---|
| Docker Hub | bearyj/vitalforge-weight |
bearyj/vitalforge-dashboard |
| GHCR | ghcr.io/bearyjd/vitalforge-weight |
ghcr.io/bearyjd/vitalforge-dashboard |
Or pull individually:
docker pull bearyj/vitalforge-weight:latest
docker pull bearyj/vitalforge-dashboard:latestgit clone https://github.com/bearyjd/vitalforge.git
cd vitalforge
cp .env.example .envEdit .env with your credentials:
GARMIN_EMAIL=your_garmin_email@example.com
GARMIN_PASSWORD=your_garmin_password
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
VITALFORGE_USER=admin
VITALFORGE_PASS=your-password-here
VITALFORGE_SECRET=your-random-secret-here
docker compose up --buildcurl http://localhost:8085/health
curl http://localhost:8086/healthVisit http://localhost:8085 for weight logging and http://localhost:8086 for the dashboard.
| Variable | Required | Description |
|---|---|---|
GARMIN_EMAIL |
Yes | Your Garmin Connect email |
GARMIN_PASSWORD |
Yes | Your Garmin Connect password |
ANTHROPIC_API_KEY |
No | Claude API key for AI recommendations (rules engine works without it) |
ANTHROPIC_BASE_URL |
No | Custom API base URL (e.g. http://localhost:4000 for LiteLLM proxy) |
VITALFORGE_USER |
No | Login username (default: admin) |
VITALFORGE_PASS |
No | Login password. If empty, auth is disabled (open access) |
VITALFORGE_SECRET |
No | Secret key for signing session cookies |
WEIGHT_URL |
No | Public URL for weight service (e.g. https://weight.yourdomain.com) |
DASHBOARD_URL |
No | Public URL for dashboard service (e.g. https://health.yourdomain.com) |
DEFAULT_UNIT |
No | Default weight unit: lbs or kg (default: lbs) |
TZ |
No | IANA timezone for timestamps (e.g. America/New_York). Omit for browser default |
Generate a random secret:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"vitalforge/
├── shared/ # Shared Python modules
│ ├── auth.py # Cookie-based session authentication
│ ├── database.py # SQLite connection and schema setup
│ └── garmin_client.py # Garmin Connect API wrapper (garminconnect)
├── vitalforge-weight/ # Weight logging PWA service
│ ├── app.py # FastAPI app — weight CRUD + Garmin push
│ ├── templates/index.html # Mobile-first weight entry UI
│ └── static/ # PWA manifest, service worker, icons
├── vitalforge-dashboard/ # Health dashboard service
│ ├── app.py # FastAPI app — metrics API + sync
│ ├── sync.py # Garmin data sync (scheduled + manual)
│ ├── recommendations.py # Hybrid rules + LLM recommendation engine
│ ├── templates/index.html # Dashboard UI with Chart.js visualizations
│ └── static/ # PWA manifest, service worker
├── nginx/ # Reverse proxy config for custom domains
├── docker-compose.yml # Development — builds from source
├── docker-compose.prod.yml # Production — pulls from GHCR
└── .github/workflows/ # CI/CD — builds and pushes Docker images
- Data volume — SQLite database and Garmin auth tokens persist in a Docker volume at
/app/data - Docker health checks — Both containers report health via
/healthendpoint - Non-root containers — Entrypoint fixes volume permissions, then drops to dedicated
vitalforgeuser - CI/CD — GitHub Actions builds and pushes images to GHCR on every push to
main
The dashboard automatically syncs data from Garmin Connect every 2 hours. You can also trigger a manual sync from the dashboard UI. Synced metrics:
- Sleep duration and sleep score
- Resting heart rate and HRV
- Body Battery (daily high/low)
- Stress levels
- VO2 Max
- Weight and body fat %
- Training load
- Steps and active calories
The recommendations feature uses a hybrid approach:
- Rules engine — Detects patterns like consecutive poor sleep, elevated RHR, declining HRV, overtraining risk, and cross-metric correlations
- LLM layer (optional) — Sends findings to Claude API for personalized, actionable recommendations
If no ANTHROPIC_API_KEY or ANTHROPIC_BASE_URL is set, the system falls back to rules engine output only.
Cookie-based session auth with a 30-day expiry. Set VITALFORGE_PASS in .env to enable. Both services share the same credentials. Leave VITALFORGE_PASS empty to disable auth (open access).
Images are automatically built and pushed to Docker Hub and GHCR on every push to main:
bearyj/vitalforge-weight:latest/ghcr.io/bearyjd/vitalforge-weight:latestbearyj/vitalforge-dashboard:latest/ghcr.io/bearyjd/vitalforge-dashboard:latest
Tagged releases (v1.0.0) also produce versioned image tags.
# On your server
mkdir vitalforge && cd vitalforge
curl -O https://raw.githubusercontent.com/bearyjd/vitalforge/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/bearyjd/vitalforge/main/.env.example
cp .env.example .env
# Edit .env with your credentials
docker compose -f docker-compose.prod.yml up -ddocker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -dCopy nginx/nginx.conf to your nginx configuration and update the server_name values:
server_name weight.yourdomain.com;
server_name dashboard.yourdomain.com;
The nav bar in each service automatically detects whether you're behind nginx (subdomain routing) or using direct ports.
For SSL, add Let's Encrypt with certbot:
sudo certbot --nginx -d weight.yourdomain.com -d dashboard.yourdomain.comBoth services are installable as Progressive Web Apps:
- Desktop: Visit
http://localhost:8085in Chrome, click the install icon in the address bar - Android (local): Connect phone via USB, use Chrome DevTools port forwarding, visit
localhost:8085on phone - Android (production): Visit
https://weight.yourdomain.com, tap "Add to Home Screen" - Quick access: Use ngrok (
ngrok http 8085) for a temporary HTTPS URL to install from
Log weight from your Android phone using Tasker without opening the browser.
- Create a new Task in Tasker
- Add action: Input > Input Dialog
- Title:
Weight - Input Type:
Decimal
- Title:
- Add action: Net > HTTP Request
- Method:
POST - URL:
https://weight.yourdomain.com/api/weight - Headers:
Content-Type: application/json - Body:
{"weight": %input, "unit": "lbs"} - If using auth, add header:
Cookie: vf_session=YOUR_SESSION_COOKIE
- Method:
- Add action: Alert > Flash
- Text:
Weight logged: %input lbs
- Text:
- Go to Tasker > Tasks > long-press your weight task
- Select "Add Shortcut to Home"
- One tap to log weight from your home screen
Since VitalForge uses cookie-based auth, the easiest approach:
- Log in via browser and copy the
vf_sessioncookie value from dev tools - Add as header in HTTP Request:
Cookie: vf_session=YOUR_COOKIE_VALUE - Session lasts 30 days — log in again when it expires
Pair an NFC tag with your scale for a tap-to-log workflow:
- Get an NFC tag — NTAG213 stickers work well, attach one to or near your scale
- Install Tasker + AutoNFC plugin (or use Tasker's built-in NFC support)
- Write the tag — Use any NFC writer app to write a unique identifier
- Create a Tasker Profile:
- Trigger: Event > Net > NFC Tag (select your tag)
- Link to the weight-logging task above
- Workflow: Step on scale, read weight, tap phone to NFC tag, enter weight, done
If you don't use Tasker, the free "NFC Tools" app can open a URL on tap:
- Write a URL record to the tag:
https://weight.yourdomain.com - Tapping the tag opens the weight PWA directly
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/ |
Weight entry UI |
POST |
/api/weight |
Log weight ({"weight": 185.4, "unit": "lbs"}) |
GET |
/api/weight/recent |
Last 10 weigh-ins |
GET |
/api/weight/trend |
Last 30 days for trend chart |
DELETE |
/api/weight/{id} |
Delete a weigh-in |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/ |
Dashboard UI |
POST |
/api/sync?days=7 |
Trigger manual Garmin sync |
GET |
/api/sync/status |
Last sync time and status |
GET |
/api/metrics/{name}?days=30 |
Time series data with 7-day moving average |
GET |
/api/recommendations |
AI-powered health recommendations |
GET |
/api/recommendations/rules-only |
Rules engine output without LLM |
Available metrics: sleep_duration, sleep_score, resting_hr, hrv, body_battery, body_battery_low, stress, vo2max, weight, body_fat, training_load, steps, active_calories
MIT