# Path B Spike: Reverse-Engineer Hevy Live Workout State

Date: 2026-05-26
Owner: Chicho / Pipo
Task: Investigate how to access Hevy's live/draft/active workout state from external systems.

## VERDICT

**FEASIBLE via internal API + local device state, but NO public live-workout endpoint exists.**

Hevy does NOT expose a public "currently active workout" API endpoint. However, the live workout state IS stored locally on the Android device (React Native AsyncStorage/MobX state) AND periodically synced to the Hevy API as draft/session data. The Wear OS "Live sync" feature proves Hevy internally tracks live state. Three viable attack vectors exist:

1. **Internal API endpoint discovery** (network capture) — HIGHEST VALUE
2. **Local device state extraction** (ADB + AsyncStorage) — MOST RELIABLE
3. **Web app browser state** (DevTools + localStorage/state) — LOWEST FRICTION

The recommended strategy is **#1 (network capture) + #2 (local state)** combined, giving both remote and local access paths.

---

## KEY DISCOVERIES

### 1. Hevy Tech Stack (confirmed)
- **Mobile**: React Native (TypeScript), MobX state management
- **Shared lib**: `hevy-shared` npm package (v1.0.906) — includes websocket.js, chat.js, constants.js
- **Web app**: React/Vue at hevy.com
- **Package**: `com.hevy`, MainActivity: `com.hevy.MainActivity`
- **APK size**: ~156 MB (latest 3.0.13, arm64-v8a)

### 2. Internal API Endpoints (discovered by dmzoneill/hevyapp-api)

**Authentication**:
- POST `https://api.hevyapp.com/login` — `x-api-key: shelobs_hevy_web` (web) or `klean_kanteen_insulated` (mobile)
- POST `https://api.hevyapp.com/v1/oauth/token` — OAuth2 (used by Hevy-Insights)
- Returns: `auth_token` (Bearer JWT)

**Internal endpoints** (not in public API docs):
- GET `/account` — user profile
- GET `/workout_count` — total workouts
- GET `/feed_workouts_paged` — paginated feed
- GET `/routines_sync_batched` — batch routine sync
- GET `/user_preferences` — user settings
- GET `/set_personal_records` — personal records

**Key finding**: Internal API works WITHOUT Hevy Pro subscription! (confirmed by gab-es21/hevy-workout-generator)

### 3. Potential Live State Endpoints (TO DISCOVER)

Based on the Wear OS sync feature and `hevy-shared` websocket.js, the following endpoint patterns are LIKELY to exist:

```
POST /live_workout/start
POST /live_workout/set_complete
POST /live_workout/exercise_complete
GET  /live_workout/state
PUT  /live_workout/finish
DELETE /live_workout/cancel

# Or REST-style:
POST /workout_sessions
PUT  /workout_sessions/{id}
GET  /workout_sessions/{id}
GET  /workout_sessions/active

# Or WebSocket:
wss://api.hevyapp.com/ws  (or similar)
```

### 4. Local Storage Paths (Android)

```
# React Native AsyncStorage (plain text SQLite)
/data/data/com.hevy/databases/RKStorage

# SharedPreferences
/data/data/com.hevy/shared_prefs/*.xml

# App cache
/data/data/com.hevy/cache/
/data/data/com.hevy/files/

# WebView data
/data/data/com.hevy/app_webview/
```

### 5. Web App Local Storage

The Hevy web app (hevy.com) stores:
- `hevy_auth_token` / `hevy_access_token` in localStorage
- Auth in HttpOnly cookies
- Workout state likely in React/Vue component state or IndexedDB

### 6. Wear OS Sync Mechanism
- Uses Google's Wear OS Data Layer API (not cloud API)
- Syncs via Bluetooth/WiFi Direct between phone and watch
- "Live sync your Wear OS Watch workout with your phone"
- This proves Hevy HAS internal live workout state tracking
- The phone app holds the canonical live state

---

## STRATEGY 1: Network Capture (mitmproxy)

**Goal**: Discover hidden API endpoints for live workout state by intercepting Hevy mobile/web traffic during a workout.

### Prerequisites
```bash
# Install mitmproxy on Ubuntu server
sudo apt-get install -y mitmproxy

# Install ADB (already on Chicho's machine)
sudo apt-get install -y adb android-tools-adb
```

### Option A: Android mitmproxy (PREFERRED)

```bash
# Step 1: Start mitmproxy on Ubuntu server
mitmweb --listen-port 8080 --web-port 8081 &
# Or: mitmproxy -p 8080

# Step 2: On Pixel 9a, set proxy
# Settings → Wi-Fi → [network] → Advanced → Proxy → Manual
# Host: 100.87.116.90 (Tailscale IP of Ubuntu server)
# Port: 8080

# Step 3: Install mitmproxy CA cert on Pixel 9a
# Open http://mitm.it in browser, download Android cert
# Settings → Security → Install from storage → select cert

# Step 4: Bypass certificate pinning (React Native may pin)
# Use apk-mitm to patch APK:
npm install -g apk-mitm
# Download APK from apkpure:
# https://m.apkpure.com/hevy-gym-log-workout-tracker/com.hevy/download
apk-mitm hevy.apk
adb install hevy-patched.apk

# Step 5: Start workout in Hevy app and observe all API calls
```

### Option B: Web App DevTools (SIMPLER FIRST STEP)

```bash
# Open Chrome on any machine
# 1. Go to hevy.com and log in
# 2. F12 → Network tab
# 3. Filter: api.hevyapp.com
# 4. Start a workout on the web app
# 5. Observe all XHR/fetch requests
# 6. Copy requests as cURL/fetch for analysis
```

### Option C: Web App mitmproxy

```bash
# Export mitmproxy CA cert and install in browser
# Set browser proxy to Ubuntu server:8080
# Browse hevy.com and start workout
# All HTTPS traffic decrypted
```

### Expected Discoveries
- Live workout start/draft creation endpoint
- Set completion sync endpoint
- Exercise transition endpoint
- Workout state polling or WebSocket endpoint
- Actual request/response shapes for live state

---

## STRATEGY 2: Local Device State Extraction (ADB)

**Goal**: Extract Hevy's local React Native AsyncStorage data during an active workout.

### Exact Commands

```bash
# Step 1: Connect to Pixel 9a via ADB
adb devices
# Should show: [serial] device

# Step 2: Check if device is rooted or debug build
adb shell whoami
# If "shell" user → use run-as (needs debuggable=false workaround)

# Step 3: Pull entire Hevy app data (if run-as works)
adb shell run-as com.hevy ls -la /data/data/com.hevy/
adb shell run-as com.hevy ls -la /data/data/com.hevy/databases/
adb shell run-as com.hevy ls -la /data/data/com.hevy/shared_prefs/

# Step 4: Extract AsyncStorage (React Native local state)
adb shell "run-as com.hevy cat /data/data/com.hevy/databases/RKStorage" > hevy_rkstorage.db
# This is a SQLite DB with plain-text key-value pairs

# Step 5: Query the database
sqlite3 hevy_rkstorage.db ".schema"
sqlite3 hevy_rkstorage.db "SELECT * FROM catalystLocalStorage;" 2>/dev/null || \
sqlite3 hevy_rkstorage.db "SELECT * FROM AsyncStorageKV;" 2>/dev/null

# Step 6: Dump all keys looking for workout state
sqlite3 hevy_rkstorage.db ".dump" | grep -i -E "workout|session|draft|active|live|current|exercise|set|timer"
```

### Alternative: Full Backup (if run-as fails)

```bash
# ADB backup (may work depending on Android version and app flags)
adb backup -f hevy_backup.ab com.hevy
# Convert to tar
dd if=hevy_backup.ab bs=24 skip=1 | openssl zlib -d > hevy_backup.tar
tar xvf hevy_backup.tar

# Or use bmgr (Android 12+)
adb shell bmgr backup com.hevy
adb shell bmgr restore com.hevy
```

### Key Storage Locations

```
/data/data/com.hevy/
├── databases/
│   ├── RKStorage          ← React Native AsyncStorage (PRIMARY TARGET)
│   ├── AsyncStorage.db    ← Alternative RN storage
│   └── hevy.db            ← Possible custom DB
├── shared_prefs/
│   ├── com.hevy_preferences.xml
│   ├── Firebase.xml
│   └── *.xml
├── files/
│   └── *.json             ← Possible offline cache
└── cache/
    └── *.json
```

---

## STRATEGY 3: APK Static Analysis

**Goal**: Find live workout state management code in decompiled APK.

```bash
# Step 1: Download APK
wget -O hevy.apk "https://d.apkpure.com/b/APK/com.hevy?version=latest"
# Or from APKMirror

# Step 2: Decompile with jadx
jadx -d hevy_decompiled/ hevy.apk

# Step 3: Search for workout state strings
cd hevy_decompiled/
grep -r -i "live_workout\|active_workout\|workout_session\|draft\|startWorkout\|workoutState" --include="*.java" .
grep -r -i "websocket\|WebSocket\|socket\.io" --include="*.java" .
grep -r -i "api.hevyapp.com" --include="*.java" .

# Step 4: Look at the React Native bundle
# The JS bundle is in assets/index.android.bundle
# It's minified but readable
cat assets/index.android.bundle | grep -oP 'https?://[^"'\''\s]+' | sort -u
cat assets/index.android.bundle | grep -i "workout.*state\|workoutState\|activeWorkout\|liveWorkout"
```

### Key Files to Examine
- `sources/com/hevy/` — Main app code
- `assets/index.android.bundle` — React Native JS bundle (CONTAINS ALL API CALLS!)
- `resources/AndroidManifest.xml` — Permissions, services, receivers
- `resources/res/values/strings.xml` — String constants

---

## STRATEGY 4: WebSocket Discovery

**Goal**: Determine if live state uses WebSocket for real-time sync.

```bash
# In browser DevTools on hevy.com:
# 1. Go to Network tab
# 2. Filter: WS (WebSocket)
# 3. Start a workout
# 4. Observe WebSocket connections

# Check hevy-shared websocket.js content:
npm pack hevy-shared
tar xzf hevy-shared-*.tgz
cat package/built/websocket.js
cat package/built/chat.js
```

From `hevy-shared` npm registry:
- `websocket.js` (282B) — Thin wrapper, likely connects to: `wss://api.hevyapp.com/ws` or similar
- `chat.js` (484B) — Chat/messaging types
- `notifications.js` (246B) — Push notification types

---

## STRATEGY 5: Hevy Insights OAuth2 Login (Free Tier API Access)

**Goal**: Get Bearer token without Pro subscription for API exploration.

```python
# Based on casudo/Hevy-Insights approach
import requests

# Step 1: Login to get JWT
response = requests.post(
    "https://api.hevyapp.com/login",
    headers={
        "x-api-key": "shelobs_hevy_web",  # web client key
        "Content-Type": "application/json",
        "Origin": "https://www.hevy.com",
    },
    json={
        "emailOrUsername": "YOUR_EMAIL",
        "password": "YOUR_PASSWORD",
    }
)
auth_token = response.json()["auth_token"]

# Step 2: Use Bearer token for internal API calls
headers = {
    "Authorization": f"Bearer {auth_token}",
    "x-api-key": "shelobs_hevy_web",
}
# Then hit internal endpoints:
# GET /account
# GET /workout_count
# GET /feed_workouts_paged
# etc.
```

---

## DATA FLOW: What Happens During a Live Workout

Based on the Wear OS sync documentation and reverse-engineering clues:

```
Phone User taps "Start Workout"
  ↓
Hevy app (React Native) creates local workout state in MobX store
  ↓
AsyncStorage persisted: { activeWorkout: { id, start_time, exercises[], currentExercise, ... } }
  ↓
Network: POST /workout_sessions (or similar) → returns session ID
  ↓
User completes a set → local state updated
  ↓
Network: POST /live_workout/sets (or PUT /workout_sessions/{id})  
  ↓
Wear OS: Data Layer sync via Bluetooth — phone pushes latest state to watch
  ↓
User completes workout → local state finalized
  ↓
Network: POST /workouts (or /v1/workouts) → server creates completed workout
  ↓
Local state cleared
```

---

## IMMEDIATE NEXT EXPERIMENTS (in priority order)

### Experiment 1: Web App Network Capture (30 min)
```bash
# EASIEST starting point. No ADB needed.
1. Open Chrome → hevy.com → login
2. F12 → Network tab → filter "api.hevyapp.com"
3. Start a workout (any routine)
4. Observe ALL requests to api.hevyapp.com
5. Copy each unique endpoint path
6. Note: method, URL, request body, response body
7. Save HAR file for analysis
```

### Experiment 2: ADB Local State Extraction (30 min)
```bash
# REQUIRES: Pixel 9a connected via ADB
1. adb devices  # verify connection
2. Start a workout in Hevy on the phone
3. adb shell run-as com.hevy ls /data/data/com.hevy/databases/
4. adb shell run-as com.hevy cat /data/data/com.hevy/databases/RKStorage > rkstorage.db
5. sqlite3 rkstorage.db ".dump" | grep -i workout
6. Document the key structure for active workout state
```

### Experiment 3: APK Decompile + Bundle Analysis (1 hour)
```bash
1. wget APK from apkpure
2. jadx -d hevy_decompiled/ hevy.apk
3. Extract index.android.bundle
4. Search for API endpoint strings
5. Search for WebSocket URLs
6. Search for MobX store names related to workout state
```

### Experiment 4: hevy-shared Module Analysis (15 min)
```bash
1. npm pack hevy-shared
2. Extract and examine built/websocket.js
3. Extract and examine built/constants.js (API URLs)
4. Extract and examine built/index.js (type definitions)
```

### Experiment 5: Hevy Insights Code Analysis (30 min)
```bash
1. git clone https://github.com/casudo/Hevy-Insights
2. Study the OAuth2 login flow in fastapi_server.py
3. Study how they proxy internal API calls
4. Look for any workout session/draft endpoints they discovered
```

---

## TOOLS SUMMARY

| Tool | Purpose | Install |
|------|---------|---------|
| mitmproxy/mitmweb | HTTPS interception proxy | `apt install mitmproxy` |
| apk-mitm | Remove cert pinning from APK | `npm install -g apk-mitm` |
| adb | Android device bridge | `apt install adb` |
| jadx | APK decompiler to Java | `apt install jadx` or GitHub release |
| sqlite3 | Query AsyncStorage DB | `apt install sqlite3` |
| Chrome DevTools | Web app network capture | Built into Chrome |
| npm | Package analysis | Already installed |
| curl/httpie | API exploration | `apt install curl` |

---

## RISK ASSESSMENT

| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Hevy uses cert pinning (bypassable) | High | Medium | Use apk-mitm or Frida |
| Android 14+ blocks user CA certs | Medium | High | Use Android ≤13 for capture, or root |
| Internal API changes break integration | Medium | Medium | Version-lock, graceful degradation |
| Live state not exposed via REST at all | Medium | High | Fall back to local AsyncStorage extraction |
| Token expiration (15-20 min) | High | Low | Implement refresh or use Pro API key |
| Hevy legal/TOS issues | Low | Medium | This is for OWN account; not commercial |

---

## FILES IN THIS DIRECTORY

- `README.md` — This file (comprehensive spike report)
- `internal-api-endpoints.md` — Documented internal API endpoints
- `adb-extraction-commands.md` — Step-by-step ADB extraction guide
- `network-capture-guide.md` — mitmproxy setup and capture instructions
- `apk-analysis-guide.md` — APK decompile and bundle analysis
