Your health stack, finally unified.
Garmin · Hevy · Strava — one self-hosted dashboard, your data, your server.
Daily steps, resting heart rate, stress trend, body composition, recovery status, weekly training load, activity streaks — your full health picture on one page.
Pace progression across every run, monthly mileage history, VO2max trend, training zones, HR vs pace scatter, split analysis, shoe mileage tracking.
Muscle activation map (front & back) with volume by muscle group across all sessions. Exercise progression, personal records, gym frequency heatmap.
Sleep stages over time (deep, light, REM, awake), score trend, sleep schedule, HRV, blood oxygen, respiration rate, body battery, training readiness.
See exactly what synced, configure push rules (e.g. Hevy strength → Strava), and monitor the live data pipeline.
Garmin Connect ──┐
├──▶ TS sync (Actions + Vercel crons) ──▶ Neon PostgreSQL ◀── web/ (Next.js) ──▶ Vercel
Hevy API ────────┘ │
▼
Strava (push)
The stack is all TypeScript. The ingest/routing pipeline runs as GitHub Actions
(.github/workflows/sync.yml → scripts/sync-pipeline.mts)
plus Vercel cron functions, composing the same lib modules the web/ app reads.
Shared logic is published as npm packages — macro-engine-core,
banister, run-dj,
garmin-auth — and the UI (web/ + the cross-platform
universal/ Expo app) renders from the shared soma-style design system.
| Service | Required | Notes |
|---|---|---|
| Neon | ✓ | Free tier works |
| Garmin Connect | ✓ | Any Garmin device |
| Hevy Pro | optional | Strength workouts |
| Strava API app | optional | Activity push |
# Apply the schema migrations (in order)
for f in web/lib/db/migrations/*.sql; do psql "$DATABASE_URL" -f "$f"; doneSet Root Directory → web in Vercel, then add:
| Variable | Value |
|---|---|
DATABASE_URL |
Neon connection string |
AUTH_SECRET |
Random 32-char string (openssl rand -base64 32) |
GITHUB_CLIENT_ID |
From your GitHub OAuth app |
GITHUB_CLIENT_SECRET |
From your GitHub OAuth app |
GITHUB_OWNER_USERNAME |
Your GitHub username (only this account gets access) |
NEXT_PUBLIC_BASE_URL |
Your Vercel URL |
GITHUB_PAT |
GitHub Personal Access Token with repo:workflow scope — needed for the Sync Now button |
STRAVA_CLIENT_ID |
From your Strava API settings (optional) |
STRAVA_CLIENT_SECRET |
From your Strava API settings (optional) |
GitHub OAuth app setup: go to github.com/settings/developers → New OAuth App → set Authorization callback URL to https://your-app.vercel.app/api/auth/callback/github.
Then visit /connections to complete Strava OAuth.
GitHub Actions (recommended — free, runs in the cloud)
Add to your fork under Settings → Secrets → Actions:
| Secret | Value |
|---|---|
DATABASE_URL |
Neon connection string |
GARMIN_EMAIL |
Garmin Connect email |
GARMIN_PASSWORD |
Garmin Connect password |
HEVY_API_KEY |
Hevy API key (Pro required) |
STRAVA_CLIENT_ID |
Strava client ID |
STRAVA_CLIENT_SECRET |
Strava client secret |
TELEGRAM_BOT_TOKEN |
(optional) |
TELEGRAM_CHAT_ID |
(optional) |
Runs every 4 hours via .github/workflows/sync.yml. Trigger instantly via the Sync Now button on the Connections page, or from the Actions tab.
cd web && npm install && npm run dev # → http://localhost:3456
cd web && npx tsx scripts/sync-pipeline.mts # manual sync run (TS pipeline)
cd universal && npm install && npm run web # cross-platform Expo app (RN Web)A floating chat bubble in the bottom-right of every soma page spawns a
local claude -p subprocess per turn — no API key, no extra service. It
reuses your Claude Code subscription auth from ~/.claude/.
Requirements:
- Claude Code CLI installed and authenticated (
claude --versionshould print a version). - soma running locally (
npm run devabove). The widget makes a subprocess call, so it can't work on Vercel deployments — local-only by design.
On the first message the route bootstraps a new session, captures its UUID,
and stores it at ~/.soma/chat.json so subsequent turns resume the same
conversation. Click the pencil icon in the chat header to start a fresh
thread.
The assistant is primed by web/lib/chat-system-prompt.md — role, project layout, DB schemas, conventions. Edit that file to change how it behaves.
You can have the deployed soma forward chat requests to your Mac's local Claude
Code via a Cloudflare tunnel. Vercel becomes a thin proxy; the actual
claude -p still runs on your machine against your subscription auth.
On the Mac (one-time setup):
brew install cloudflared
cloudflared tunnel login # browser auth
cloudflared tunnel create soma-chat # creates UUID + creds
cloudflared tunnel route dns soma-chat chat.your-domain.com # your subdomain~/.cloudflared/config.yml:
tunnel: soma-chat
credentials-file: /Users/<you>/.cloudflared/<UUID>.json
ingress:
- hostname: chat.your-domain.com
service: http://localhost:3456
- service: http_status:404Then run it (or launchctl it for autostart):
cloudflared tunnel run soma-chatAdd a shared secret to web/.env.local:
SOMA_CHAT_TOKEN=$(openssl rand -base64 32)
Keep npm run dev running on the Mac as usual.
On Vercel (Settings → Environment Variables):
| Var | Value |
|---|---|
SOMA_CHAT_TUNNEL_URL |
https://chat.your-domain.com |
SOMA_CHAT_TOKEN |
same value as the Mac's .env.local |
Redeploy. The widget now shows a status dot in the header:
- 🟢 green — direct local subprocess (when you're on
localhost:3456) - 🔵 blue — proxied via Vercel → your Mac's tunnel (when you're on the deployed URL)
- 🔴 red — tunnel unreachable (Mac asleep, no internet, etc.)
Rotate SOMA_CHAT_TOKEN on both sides if it ever leaks. The shared secret is
the only thing standing between your tunnel's public DNS name and your
local filesystem.
Two plists under ~/Library/LaunchAgents/ (per-user, no sudo) keep
everything alive across Mac sleep/wake/reboot:
| Plist | Purpose |
|---|---|
dev.gkos.soma.web.plist |
Runs npm run dev from web/ on :3456, KeepAlive on crash |
dev.gkos.soma.tunnel.plist |
Runs the cloudflared named tunnel (tunnel run --token <connector>), KeepAlive |
Logs land in ~/Library/Logs/soma/. Manage with
launchctl bootstrap gui/$UID <plist> to load and
launchctl bootout gui/$UID dev.gkos.soma.web to temporarily stop for
hands-on development.
If your domain's authoritative DNS lives somewhere other than Cloudflare,
you can still use a Cloudflare Quick Tunnel — but its trycloudflare.com
URL rotates on every cloudflared restart. To keep the deployed Vercel
chat pointed at the live URL automatically, add a third agent that tails
the tunnel log and syncs URL changes into Vercel env:
| Plist | Purpose |
|---|---|
dev.gkos.soma.tunnel-watch.plist |
Runs web/scripts/soma-tunnel-watch.sh — tails the tunnel log, detects URL rotation, runs vercel env rm/add SOMA_CHAT_TUNNEL_URL production + vercel deploy --prod |
Prereq: the Vercel CLI must be logged in (vercel login) once on the Mac
so the watcher's CLI calls work non-interactively.
This becomes unnecessary once you migrate your zone to Cloudflare DNS (the named-tunnel route in the previous section), since the URL is then stable.