**Shape A. No question.** The latency compounds — a review session that fires 40 GETs per minute is paying 400ms-1200ms of pure network tax every minute, and that's before Tailscale has a bad WiFi moment. Every one of those reads is hitting files that already exist on the MacBook via Dropbox. You're paying for round trips to read your own files.

Here's where to draw the lines:

---

**1. Read-local, write-remote.** This is the whole architecture in four words.

MacBook runs Vite + FastAPI against the Dropbox-synced tree. All GET routes resolve locally — fast `iterdir()`, fast JSON reads, fast media serving. Mutations (PUT/POST/DELETE on shot state) proxy to Studio's API over Tailscale. Writes are rare during review sessions (changing a beat status, adding a note) and 10-30ms on a single write is imperceptible. You get the latency win on the read-heavy path without touching the conflict problem.

**Do not allow symmetric writes.** Dropbox conflict resolution on JSON is poison — it silently creates `shots/shot_003 (1).json` and your API never sees it. One writer, one truth. Studio owns mutations. This is non-negotiable.

The implementation is a lightweight proxy middleware in FastAPI — maybe 30 lines. When `RECOIL_WRITE_UPSTREAM` is set, any non-GET request gets forwarded to that URL with the same path and body. When it's unset (Studio's own instance), requests hit the local handlers directly. Same codebase, no branching logic in route handlers.

---

**2. ttyd host: runtime config from the API, not an env var.**

Bake it into the existing `/api/system-status` response or a new `/api/config` endpoint. The frontend fetches it once on load and constructs iframe URLs from it. Reasons: no rebuild to switch modes, the frontend adapts if you ever move ttyd, and if you fall back to all-Studio access the same build just works. One env var on the FastAPI side (`TTYD_HOST`, defaults to `localhost`) feeds the response.

---

**3. Gotchas you missed:**

- **Partially-synced media files.** Studio writes a 40MB MP4. Dropbox starts syncing. MacBook's API gets a request for it 5 seconds later and serves a truncated file. For media, consider a fallback: if the local file's mtime is less than N seconds old and size is under some threshold, proxy the request to Studio instead. Or just accept that freshly-generated media might take 30 seconds to be viewable locally — during active generation you're probably watching Studio's output anyway.

- **Dropbox extended attributes and `.dropbox.cache`.** Python's `iterdir()` can surface `.dropbox` dotfiles and extended-attribute sidecars. Your `projects/` listing code should already filter dotfiles, but verify — a phantom project called `.dropbox.cache` in the sidebar would be confusing.

- **No auto-restart on MacBook.** Studio has launchd. MacBook doesn't need it — you open the laptop, you start the dev session. A one-liner in `.zshrc` or a tmux startup script is fine. Don't add a LaunchAgent; it'll fight you when you want to iterate on the code.

- **Vite HMR noise from Dropbox syncs.** When Studio writes files (overnight generation, Claude sessions), Dropbox syncs them to MacBook and Vite sees filesystem events. For source code this triggers HMR — harmless but noisy. For project data files (shots JSON, media), Vite shouldn't be watching those paths anyway. Verify your Vite config excludes `projects/` from the watch tree.

---

**4. Concrete first step — the 20-minute version:**

1. On MacBook, `cd ~/Dropbox/CLAUDE_PROJECTS/recoil && source .venv/bin/activate`.
2. Add one env var: `export RECOIL_WRITE_UPSTREAM=http://100.105.59.118:8431`.
3. Add the proxy middleware to FastAPI (intercept non-GET, forward to upstream, return response).
4. Start FastAPI locally: `uvicorn app:app --port 8431 --reload`.
5. Start Vite locally: `pnpm dev` (already configured for `:5173`).
6. Open `http://localhost:5173`. Reads are instant. Writes proxy to Studio. Chat iframes point to Studio via the config endpoint.

That's it. You'll feel the difference on the first project load. The shot grid that used to take 800ms now takes 50ms because you're reading local JSON instead of round-tripping 30 fetches over Tailscale.

---

**Summary:** Local read-only replica with write-proxy to Studio. One middleware, one env var, no Dropbox conflicts. Studio keeps its role as the durable compute node — overnight runs, ttyd sessions, canonical writes. MacBook becomes the fast interactive surface it should have been from the start.
