A phone-browser workout logger that turns manual or Bee transcripts into pending set candidates, confirmed sets, and owner-hosted workout history.
React dev UI:
npm install
npm run dev -- --host 0.0.0.0 --port 5176Then open http://localhost:5176/static/ on the Mac.
For the real iPhone workflow, keep that server running and open the Mac's LAN URL on the phone:
LAN_IP="$(ipconfig getifaddr en0 || ipconfig getifaddr en1)"
echo "http://$LAN_IP:5176/static/"Use that URL in Safari on the iPhone. This is the fastest local proof path because the phone is exercising the same browser/PWA surface that goes to Fly later.
Build the React app into the FastAPI-served static/ directory:
npm run buildFastAPI/static preview:
uv run python main.pyThen open http://localhost:8080. The runtime binds to 0.0.0.0:$PORT and defaults to PORT=8080.
Health check:
curl http://localhost:8080/healthSelf-serve guided path:
scripts/setup-demo.sh --install-toolsThe script checks local tools, logs into Fly, creates or selects a Fly app, creates a bee_data volume mounted at /data, asks for your OpenAI API key, enables Bee QR linking, deploys, waits for /health, opens the app, and prints the Bee Developer Mode steps.
It also generates the private owner/setup URL and stores it in gitignored local state:
.gymscribe/private-setup-url.txt
.gymscribe/private-setup-qr.png
.gymscribe/private-setup-qr.svg
.gymscribe/setup.env
Use it later from the computer with:
open "$(cat .gymscribe/private-setup-url.txt)"Use it later from a phone by opening the saved QR and scanning it:
open .gymscribe/private-setup-qr.pngThat URL and QR are secrets. They contain the generated owner key and Bee setup key. The same keys are stored as Fly secrets, while .gymscribe/ is only for this local clone and is ignored by git. The OpenAI API key is different: the script sends it to Fly as OPENAI_API_KEY but does not save the key in .gymscribe/.
The private setup URL is not just a bookmark. It contains two secret values in the fragment:
APP_ACCESS_KEY unlocks the owner session, workout history, and Bee sync
BEE_LINK_ADMIN_KEY unlocks Bee QR linking and unlinking
Those values are also stored as Fly secrets. If either value changes on Fly, every older private setup URL that contains the previous value is stale. This is why an old chat-pasted URL can stop working even when the Fly Machine is healthy.
The local source of truth for this clone is the URL plus QR stored under .gymscribe/:
open "$(cat .gymscribe/private-setup-url.txt)"
open .gymscribe/private-setup-qr.pngBy default, scripts/setup-demo.sh reuses .gymscribe/setup.env so it does not casually rotate keys. To intentionally rotate owner/setup access:
scripts/setup-demo.sh --rotate-keys --app bee-computer-repsThat command replaces the Fly APP_ACCESS_KEY, BEE_LINK_ADMIN_KEY, and SESSION_SIGNING_KEY, then rewrites .gymscribe/private-setup-url.txt, .gymscribe/private-setup-qr.png, .gymscribe/private-setup-qr.svg, and .gymscribe/setup.env. Afterward, old private setup URLs and QR codes stop working and existing browser owner sessions need to open or scan the new stored URL/QR.
If a QR or private URL is stale because Fly secrets changed elsewhere, run the setup script again without rotation first:
scripts/setup-demo.sh --app bee-computer-repsThat reuses the local .gymscribe/setup.env keys, pushes them back to Fly secrets, and rewrites the URL/QR artifacts so the phone QR stays current for this clone. Use --rotate-keys only when you intentionally want old URLs and QR codes to stop working.
Do not rotate BEE_TOKEN_ENCRYPTION_KEY during normal access-key cleanup. That key protects the encrypted Bee token file on /data; changing it can require relinking Bee. Only rotate it when you intentionally want to invalidate the stored Bee token:
scripts/setup-demo.sh --rotate-keys --rotate-token-encryption-key --app bee-computer-repsOptional environment knobs:
FLY_APP_NAME=my-bee-reps FLY_REGION=sjc FLY_ORG=my-org scripts/setup-demo.shInstall and log in:
brew install flyctl
flyctl auth loginCreate the Fly app once. Public Fly app names are global, so change app = "bee-computer-reps" in fly.toml first if this name is already taken:
flyctl apps create bee-computer-repsUse QR linking from the app UI for the product path. For a private one-user backend fallback, you can still set a server-side Bee token:
flyctl secrets set BEE_API_TOKEN="..."Deploy:
flyctl deploy --remote-onlyThe Dockerfile is self-contained for Fly's remote builder. It runs npm ci, builds the Vite app into static/, installs Python dependencies with uv, and serves the built assets from FastAPI. You do not need to commit prebuilt static/ assets for deploys.
The app stores QR-linked Bee credentials at /data/bee-token.enc and saved workout history at /data/gymscribe.sqlite when a Fly volume is mounted. The fly.toml includes:
[mounts]
source = "bee_data"
destination = "/data"
initial_size = "1gb"Open:
flyctl openFor this MVP, every owner runs their own Fly app. Saved workout history is stored in that owner's SQLite database at /data/gymscribe.sqlite. The phone app still uses browser storage for the active draft and Bee sync cursor, but completed workouts are posted to the owner backend.
The backend does not provide accounts, teams, global sync, or shared cloud history. Treat each Fly deployment as a private one-user backend.
The default fly.toml is configured to conserve compute:
[http_service]
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[http_service.concurrency]
type = "requests"
soft_limit = 20That lets Fly stop the web Machine when it is idle and start it again on the next request. The tradeoff is a cold-start pause when opening the app after it has been idle.
Important: stopped Machines can still have tiny root filesystem charges, and the bee_data Fly volume is charged while it exists because it stores SQLite history and the encrypted Bee token. To pause compute without losing data:
flyctl machine stop --app bee-computer-reps e826247c0d0d38To remove ongoing storage charges, destroy the app or volume only after you are willing to lose the stored Bee token and workout database:
flyctl apps destroy bee-computer-repsIf the app name is already taken, choose a unique name in fly.toml and use that same name with flyctl apps create.
For the owner-ready QR path, let the helper create the app if needed, generate the owner/setup keys, set Fly secrets, deploy with the remote builder, and print both the public app URL and the private setup URL:
scripts/phone-demo.sh --install-tools --enable-linkingOpen the private setup URL on the owner phone, tap Link Bee, then scan the generated QR from Bee. The setup URL lives in a URL fragment (#appKey=...&setupKey=...), so those keys are not sent as a URL path or query string. The app exchanges them once for an HttpOnly owner session cookie and then strips the fragment from the browser address bar. The setup screen also reports whether the backend has an OpenAI key configured for draft workout extraction.
Useful deploy checks:
flyctl status
flyctl logs
curl https://bee-computer-reps.fly.dev/healthThe fastest local demo workflow is browser-first:
- Start the dev server for LAN access:
npm run dev -- --host 0.0.0.0 --port 5176- Print the phone URL:
LAN_IP="$(ipconfig getifaddr en0 || ipconfig getifaddr en1)"
echo "http://$LAN_IP:5176/static/"- Open that URL in Safari on the iPhone.
The fastest public demo workflow is the same product surface on Fly:
- Deploy:
flyctl deploy --remote-onlyThe Dockerfile builds the React app inside Fly's remote builder, so a local npm run build is optional for previewing but not required before deploy.
- Verify:
curl https://bee-computer-reps.fly.dev/health- Open:
https://bee-computer-reps.fly.dev/
For live demos, show or scan a QR code for that URL. The phone should land directly on the workout logger. Bee can be unlinked and manual transcript should still complete the golden path: transcript -> pending candidate -> confirm -> saved workout history.
Bee linking should be part of the app UI, not a hidden ops-only setup. For public publishing, each person should deploy their own Fly app and link their own Bee. Do not enter a Bee token into someone else's hosted URL.
The privacy model:
flowchart LR
Owner["Owner phone PWA<br/>normal HTTPS"] --> Backend["Own Fly backend<br/>setup-key-gated QR linking<br/>server-side Bee token"]
Backend --> Bee["Bee API<br/>private CA plus bearer token"]
Visitor["Other visitor<br/>no setup key"] -. "manual demo only" .-> Backend
The desired product flow is one simple loop:
- The workout logger shows
Bee not connectedand aLink Beeaction nearSync Bee. - The app backend creates a Bee app-pairing request and returns a QR/link such as
https://bee.computer/connect#<requestId>. - The user opens the Bee iOS app and enables Developer Mode if needed.
- In Bee, the user taps
Developer->Connect App. - Bee opens a scanner and the user scans the QR shown by the workout logger.
- The backend polls the pairing request, decrypts the returned token server-side, and keeps it out of browser localStorage.
- The workout logger changes to
Bee connected, andSync Beestarts returning normalized transcript events from/api/sync-now. - Saved workouts are written to this Fly app's SQLite database, not to a shared service.
The Bee QR scanner is the product clue: our app should generate the QR, and the Bee app should scan/approve it. Users should not paste API tokens into a phone browser.
Enable the in-app QR path locally with:
BEE_LINKING_ENABLED=true uv run python main.pyOn Fly, production QR linking requires the generated owner/setup secrets. The setup script creates them, stores them as Fly secrets, and saves the private URL locally:
scripts/setup-demo.sh --install-toolsThen open the stored private URL:
open "$(cat .gymscribe/private-setup-url.txt)"The app exchanges the URL fragment keys for an HttpOnly owner session cookie, then removes the fragment from the address bar. The Bee token stays on the backend.
The QR path is intentionally backend-owned:
POST /api/bee/link/startcreates the Bee app-pairing request.GET /api/bee/link/status/:requestIdpolls until Bee approval.DELETE /api/bee/linkclears the private demo token.- The browser receives only the QR/link and status, never the Bee token.
Pairing sequence:
sequenceDiagram
participant Phone as Owner phone PWA
participant Fly as Fly backend
participant BeeApp as Bee iOS app
participant BeeAPI as Bee pairing API
Phone->>Fly: POST /api/bee/link/start with owner session + CSRF
Fly->>BeeAPI: Create pairing request with public key
BeeAPI-->>Fly: requestId and expiry
Fly-->>Phone: QR payload
BeeApp->>Phone: Scan QR from Link Bee sheet
BeeApp->>BeeAPI: Approve app pairing
Phone->>Fly: Poll /api/bee/link/status
Fly->>BeeAPI: Poll pairing status
BeeAPI-->>Fly: Encrypted Bee token
Fly->>Fly: Decrypt and store token server-side
Fly-->>Phone: Bee connected
Screens from the Bee app:
For public-repo self-serve setup, use:
scripts/setup-demo.sh --install-toolsFor local phone iteration, use:
scripts/phone-demo.shOptional deploy path:
scripts/phone-demo.sh --install-tools --deployscripts/setup-demo.sh behavior:
- Check for
uv,npm, andflyctl; install common missing tools when--install-toolsis passed. - Prompt for a unique Fly app name.
- Verify Fly auth or open
flyctl auth login. - Install Python and Node dependencies.
- Run
uv run pytestandnpm run build. - Create the Fly app if needed.
- Create the
bee_dataFly volume in the selected region for Bee credentials and SQLite workout history. - Generate or reuse
APP_ACCESS_KEY,BEE_LINK_ADMIN_KEY,SESSION_SIGNING_KEY, andBEE_TOKEN_ENCRYPTION_KEY. - Prompt for
OPENAI_API_KEYor reuse the existing Fly secret if this clone already marked it configured. - Set those values as Fly secrets with
BEE_LINKING_ENABLED=true,OPENAI_MODEL=gpt-5.5, andOPENAI_REASONING_EFFORT=low. - Save the private setup URL, QR code, and local secret state to
.gymscribe/private-setup-url.txt,.gymscribe/private-setup-qr.png,.gymscribe/private-setup-qr.svg, and.gymscribe/setup.env. - Deploy with
flyctl deploy --remote-only --app <app-name>. - Wait for
https://<app-name>.fly.dev/health. - Open the private setup URL
https://<app-name>.fly.dev/setup#appKey=<key>&setupKey=<key>and print the Bee 5-tap / QR scan steps.
scripts/phone-demo.sh behavior:
- Check for
flyctl; install withbrew install flyctlwhen missing and--install-toolsis passed. - Check for Bee CLI; install with
npm install -g @beeai/cliwhen missing and--install-toolsis passed. - Verify Fly auth with
flyctl auth whoami. - Run
npm ciif dependencies are missing. - Create the Fly app and
bee_datavolume when deploying. - Prompt for
OPENAI_API_KEYand set it as a Fly secret when deploying. - Deploy with
flyctl deploy --remote-onlywhen--deployis passed. - Start
npm run dev -- --host 0.0.0.0 --port 5176for the local phone path, or leave the existing server alone if it is already running. - Print the Mac LAN URL for opening the workout logger on the phone.
- Print the Bee taps:
Bee app -> Settings -> About -> tap App version 5 times
Bee app -> Developer -> Connect App -> scan this app's QR
The script should not ask for or persist a Bee token in browser storage. It prints setup/app URLs only; Bee approval happens through the app QR sheet.
flyctl secrets set OPENAI_API_KEY=... --app <app-name>creates a new Fly release and restarts machines. Open the private setup URL afterward; the setup screen should show AI extraction ready. Do not put the OpenAI key in.gymscribe/, browser storage, URLs, screenshots, or frontend logs.flyctl secrets set BEE_API_TOKEN=...creates a new Fly release and restarts machines. Re-run the/healthcheck after setting it;beeConfiguredshould becometrue.- The Bee token must stay server-side. Do not put it in
localStorage, query params, screenshots, or frontend logs. - Do not share the private setup URL, private setup QR, or
.gymscribe/files. They unlock owner access for that Fly app. - The backend already owns Bee private-CA trust for the PWA path. The phone talks only to this app over normal HTTPS; the backend talks to Bee with the bundled CA, or an override from
BEE_CA_CERT. - App-pairing tokens are written to an encrypted server-side token file with owner-only permissions. Saved workouts are written to SQLite at
/data/gymscribe.sqlite. - For a durable Fly setup, keep the
bee_datavolume mounted at/data; without it, token and workout history durability depend on the machine filesystem. - The LAN iPhone URL requires the Mac and phone to be on the same network, and the macOS firewall must allow the Vite server. If that is flaky, use the Fly URL.
Enable Bee Developer Mode before trying to link or sync real Bee data:
- Open the Bee iOS app.
- Go to Settings and find the app version.
- Tap the app version 5 times to enable Developer Mode.
- Verify from your terminal:
bee login
bee statusbee status should confirm Bee Developer Mode is available before the demo tries to use Bee sync.
The product path is app-owned QR linking: tap Link Bee in the workout logger, open Bee Developer Mode, choose Connect App, and scan the QR shown by the workout logger.
For a private local fallback before durable QR-link storage exists, set a server-side Bee token before running the app:
export BEE_API_TOKEN="..."
uv run python main.pyWithout a linked Bee token, the demo still runs: manual transcript, parsing, active session state, and saved workout history work. Bee sync pulls normalized transcript events from /api/sync-now when Bee is linked; it does not write workouts to Bee.
- "Bench press 135 for 8"
- "Squat one thirty five for five"
- "8 reps of curls at 30"
- "Add 5 pounds"
- "Remember that my right knee felt tight"
- "Remind me to stretch after legs"
- "Sync to Bee"
uv run pytest


