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.
This commit is contained in:
James Pine
2026-04-25 17:09:32 -07:00
parent 2bcb98d1a8
commit f4d21504e3
49 changed files with 5229 additions and 30 deletions
+34
View File
@@ -279,3 +279,37 @@ class Capture(Base):
llm_model = Column(String, nullable=True)
refinement_flags = Column(Text, nullable=True) # JSON blob
created_at = Column(DateTime, default=datetime.utcnow)
class PairedDevice(Base):
"""A mobile device paired with this Voicebox install (V0 pair flow).
Stores only the SHA-256 of the bearer token; the bearer plaintext is
returned to the device once at pairing time and never persisted
server-side. If the device loses its bearer the user must re-pair.
"""
__tablename__ = "paired_devices"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, nullable=False)
bearer_hash = Column(String, nullable=False, unique=True, index=True)
revoked = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
last_seen_at = Column(DateTime, nullable=True)
class PairingToken(Base):
"""One-time token used to complete a device pairing.
Minted by the desktop UI via /pair/init, redeemed by the mobile
device via /pair/complete in exchange for a long-lived bearer.
Single-use; expires after ~5 minutes.
"""
__tablename__ = "pairing_tokens"
token = Column(String, primary_key=True)
expires_at = Column(DateTime, nullable=False)
used_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)