Files
voicebox/mobile/PLAN.md
T
James Pine f4d21504e3 Mobile companion app + paired-device backend
New iOS-first companion (Expo SDK 54 + NativeWind v4) with three tabs:
Captures (the hero — floating gold mic, live mic-meter waveform,
expand-row playback), Generate (profile picker + speak + autoplay +
recent), and Voices (searchable profile list).

Pairing (V0): backend mints a one-time token, mobile scans/pastes the
voicebox:// URL, server returns a long-lived bearer it stores only as a
SHA-256 hash. Bearer-or-loopback auth applied to every user-data router
so binding 0.0.0.0 doesn't leak existing endpoints. Loopback callers
(the desktop app) keep their friction-free access.

Desktop Settings → Mobile pane: live host picker (LAN / Tailscale auto-
detected via the App-bundle binary path on macOS), QR rendering,
5-minute expiry countdown, copyable URL fallback, paired-device list
with revoke. Auto-closes when a new device pairs.

just dev now binds the backend to 0.0.0.0 so paired phones can reach
it — and just setup-python pins mlx-audio==0.4.1 + mlx-lm so fresh
Apple Silicon worktrees get a working STT path on first install.
2026-04-25 17:09:32 -07:00

140 lines
8.0 KiB
Markdown

# Voicebox Mobile — V1 plan
A companion app for the Voicebox desktop. iPhone-first, dictate-anywhere, Captures as the hero. Talks to a paired desktop over Tailscale or LAN with end-to-end encryption.
V1 is entirely local — no cloud, no account. The device key minted during pairing is the root of the user's lifetime encryption identity and gets reused by the cloud phases that follow; see [`docs/plans/CLOUD_ROADMAP.md`](../docs/plans/CLOUD_ROADMAP.md) for the post-mobile arc (backup & sync → private inference → marketplace).
---
## Repo layout
- New `mobile/` at repo root, sibling to `app/`, `backend/`, `tauri/`, `landing/`, `web/`
- Standalone Expo project — not a Bun workspace member (avoids React/Tauri/Bun version drag)
- Type-sharing via OpenAPI: generate `mobile/src/api/types.ts` from backend's `/openapi.json`, commit it, regenerate on demand
- Branch off `main` once 0.5.0 ships → `feat/mobile-app`
## Stack (verified 2026-04-25)
### The SDK 54 vs SDK 55 fork
Expo SDK 55 is the current `latest` (`[email protected]`, React Native 0.83, React 19.2, New Architecture only — Legacy Architecture was dropped in 55). It also ships Expo Router v7, Hermes v1 with bytecode diffing, and `expo-brownfield`. But: **NativeWind v5 is the only NativeWind line that targets SDK 55, and v5 is still pre-release** (`5.0.0-preview.3`, with explicit "not intended for production use" warning in the v5 docs). NativeWind v4 stable (`4.2.3`) is paired with SDK 54.
Two real options:
- **Option A — Ship-fast (recommended for V1):** Expo SDK 54 + NativeWind v4.2.3. Both stable, official pairing, well-documented. Loses SDK 55's bytecode-diff updates and Expo Router v7 sugar but everything works.
- **Option B — Cutting-edge:** Expo SDK 55.0.17 + NativeWind 5.0.0-preview.3. Latest everything, but the NativeWind v5 maintainer explicitly says it's for experimentation. More breakage during dev, brittle CI.
Recommendation: **Option A**. We're trying to ship a companion app, not stress-test pre-release styling layers. We can bump to SDK 55 + NativeWind v5 once both stabilize (Expo targets stable SDK 55 mid-2026).
### Pinned versions (assuming Option A)
| Package | Version | Notes |
| --- | --- | --- |
| `expo` | `~54` (latest 54.x) | New Architecture default since SDK 51 |
| `expo-router` | bundled with SDK | file-based, typed routes |
| `nativewind` | `4.2.3` | Tailwind class parity with `app/` |
| `expo-audio` | `~54` (bundled with SDK) | stable in SDK 54+, replaces `expo-av` recording |
| `@shopify/react-native-skia` | `2.6.2` | live waveform; WaveSurfer is DOM-only |
| `expo-camera` | `~54` (bundled) | QR scan (barcode scanning built in) |
| `expo-secure-store` | `~54` (bundled) | paired device key |
| `react-native-reanimated` | `4.3.0` | rewritten for new arch |
| `react-native-gesture-handler` | `2.31.1` | |
| `zustand` | `5.0.12` | mirrors desktop |
| `@tanstack/react-query` | `5.100.5` | mirrors desktop |
(Versions for `expo-*` packages are managed by `npx expo install`, which picks the patch that matches the SDK — don't pin them by hand.)
### Other stack decisions
- **TypeScript strict**
- Theme tokens copied straight from `app/`'s shadcn theme (`hsl(43 60% 50%)` for the gold accent, dark surfaces match)
- **EAS Build** + **Dev Client** from day one — Skia and SecureStore push us off Expo Go
## Pairing & transport — Tailscale-friendly
1. Desktop: new **Settings → Mobile** with "Pair device" → renders QR + 6-digit fallback
2. QR payload: `voicebox://pair?host=<url>&secret=<b64>&fp=<sha256>`
- `host` = whatever address the user can reach: LAN IP (`192.168.x.x:17493`), Tailscale 100.x address, or MagicDNS name (`mac.tail-xxxx.ts.net:17493`) — Tailscale Just Works with zero extra code
- `secret` = one-time pairing token; mobile exchanges it for a long-lived device key on first request
- `fp` = self-signed cert fingerprint we mint at pair time, pinned on mobile
3. Auth: bearer token + XChaCha20-Poly1305 payload encryption with HKDF per-session keys — E2E layer above HTTP, survives any future cloud relay swap
## Backend additions (desktop, separate PR before mobile work)
- `POST /pair/init` — mint pairing token, return QR payload
- `POST /pair/complete` — exchange token for device-bound long-lived key, persist `paired_devices` row
- `GET/DELETE /devices` — Settings → Mobile lists & revokes paired devices, with `last_seen_at`
- Bearer middleware on `/generate`, `/transcribe`, `/profiles`, `/captures`, `/speak` (loopback callers stay unauthenticated as today; paired-device callers use the bearer)
- `/captures` upload accepts `m4a` (expo-audio's iOS default) in addition to existing formats
## Screens
### First-run
Pair flow: scan QR → confirm desktop name + fingerprint → store creds in SecureStore.
### Tab 1 — Generate
- Profile cards horizontal scroll (top)
- Recent generations list (middle) — tap to play, long-press for version picker / regenerate / share
- Floating generate box (bottom): text input + engine indicator + speak button
### Tab 2 — Voices
- List of profiles (cloned + presets), grouped by engine compatibility
- Tap to inspect: samples (Skia waveform thumbs), language, last used
- **No** profile creation in V1
### Tab 3 — Captures (the hero)
- **Big gold mic button** bottom-front-and-center — same accent as the sponsor CTA
- Tap-to-toggle in V1 (push-to-talk fights iOS gestures, defer)
- Live mic waveform via Skia, amplitude polled at 30 Hz, scrolling buffer
- Transcript text area above the waveform — empty during recording, populated on stop
- State pill at top: `recording → uploading → transcribing → refining → done` (mirrors desktop pill semantics)
- Captures list below: each row has a Skia mini-waveform, tap to expand, scrub, edit transcript inline, "Play as voice profile"
- **Audio preserved + downloadable** — visually obvious in the UI; this is the USP
- Schema/UI built so resumable capture can land later without re-architecting (no "one capture = one continuous take" assumptions baked in)
## Out of scope for V1 (deliberately)
Stories editor · Voice profile creation / sample recording · Effects editor · Personality LLM controls · Streaming transcription (lands with resumable capture in V2) · Settings beyond pairing · Android (pipeline supports it; QA focus iOS first)
## V2+ candidates
- **Resumable capture** — pause/resume that actually appends audio AND transcript (Apple Voice Notes drops the second-half transcript on resume; Voicebox shouldn't)
- **Document mode** — refinement LLM writes/edits a markdown document live as you speak, including dictated edits ("change the second bullet to…")
- **Streaming transcription** — partial transcripts during recording
- **Voice profile creation on mobile** — record samples directly
- **Android**
- **Cloud relay** — once the Voicebox platform exists, the same E2E layer rides over a relay so pairing isn't tied to Tailscale/LAN
## Build/dev workflow
```bash
cd mobile
bun install
bunx expo prebuild # generate native projects (Skia + SecureStore need it)
bunx expo run:ios # device on the same Tailnet
eas build --profile development # distributable Dev Client for TestFlight
```
Bundle ID: `sh.voicebox.mobile` (need to register in App Store Connect).
## Order of attack
1. Backend: pairing endpoints + bearer middleware (desktop PR, can land before mobile)
2. Mobile: Expo scaffold + NativeWind + theme tokens + Pair screen
3. Mobile: Captures tab end-to-end — validates transport, E2E layer, audio upload, waveform, pill states all in one flow
4. Mobile: Generate tab
5. Mobile: Voices tab
6. EAS dev build + TestFlight internal track
## Open questions
1. **iOS only V1?** Default: yes.
2. **NativeWind or hand-rolled styles?** Default: NativeWind — class parity with `app/` is worth the small bundler tax.
3. **Crib patterns from the Spacedrive mobile app first**, or start clean from current Expo docs?
4. **Bundle ID + display name** — confirm `sh.voicebox.mobile` / "Voicebox" before EAS setup.
5. **Pair screen UX** — QR-only, or always offer the 6-digit code as an a11y/fallback?