Cloud/local auth-sync, Limrun mobile dev tooling, and a Mobile Simulator pane
Summary
This PR bundles 3 related features, all under one theme: making cloud (and local) sandboxes immediately usable for desktop + mobile development without manual sign-in or a Mac/Android device.
- Auth sync - desktop and mobile auto-sign-in as the real workspace creator/developer in cloud sandboxes, and mobile auto-signs-in locally too, instead of staying signed out or falling back to a shared local-admin account.
- Limrun mobile dev tooling -
limrun-dev.sh+ vendored skills, so verifyingapps/mobileon a cloud sandbox (no local Xcode/Android SDK) is one command instead of rediscovering Limrun's quirks each time. - Mobile Simulator pane - a
Cmd+Shift+Mdesktop pane mirroring the existing Desktop/VNC pane, backed by Limrun on a sandbox or a local iOS/Android simulator on a real machine.
Part 1: Auth sync (desktop + mobile, cloud + local)
Why
Every cloud workspace's Neon branch forks from production, so real users/organizations rows already exist — there was no reason to require a manual sign-in, or to spoof a generic "local admin" identity instead of the actual developer/workspace creator.
What / How
- Desktop (cloud):
packages/auth/src/seed-cloud-auth-token.tsresolves the real creator viaSUPERSET_SANDBOX_CREATOR_USER_ID(sourced fromcloud_workspaces.created_by_user_id, threaded throughclaim.ts/sandbox-contract.ts), mints a real better-auth session server-side, and writes it through the same AES-256-GCM machine-bound encryption desktop already uses for local worktrees — extracted topackages/shared/src/auth-token-crypto.tsso a headless script can produce a file desktop can decrypt, without depending onapps/desktop. This file can't be copied across machines (the key is derived from/etc/machine-id), so it's minted fresh per sandbox rather than copied. - Mobile (cloud + local):
@better-auth/expouses cookie/SecureStore, not a portable encrypted file, so mobile instead uses better-auth'soneTimeTokenplugin:seed-cloud-mobile-token.ts(cloud) andseed-local-mobile-token.ts(local, resolves identity by validating the existingauth-token.encagainst the real API) mint a session + one-time token and bake it into.envasEXPO_PUBLIC_DEV_ONE_TIME_TOKEN.DevSignInOptionsredeems it automatically on mount if present. - The
oneTimeTokenplugin is registered unconditionally inpackages/auth/src/server.ts- a conditional entry in thepluginsarray defeats better-auth's tuple-based type inference for the wholeSession/Usertype (confirmed: brokepackages/trpc/src/router/automation/automation.ts). It's instead blocked outsidedevelopmentin ahooks.beforecheck that 404s/one-time-token/*- verified live withNODE_ENV=production. - Removed
apps/mobile/.maestro/flows/sign-in.yml- not supportable going forward and not a priority.
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Mint vs. copy auth state | Mint fresh per sandbox | auth-token.enc's key is machine-bound; copying it across machines can never work |
| Mobile mechanism | better-auth oneTimeToken plugin | Mobile's storage (cookie/SecureStore) has no equivalent to desktop's encrypted file; this is the built-in, dev-gated primitive for "redeem a token for a real session" |
| Plugin registration | Always register, gate in hooks.before | A conditional plugins array silently breaks Session/User type inference repo-wide |
Part 2: Limrun mobile dev tooling
Why
apps/mobile is iOS-only and has no local Xcode in a cloud sandbox. Verifying a change there meant hand-running several lim commands per session and rediscovering the same gotchas (SPM sandbox failures, .env not reaching the remote build, stale native projects) each time.
What / How
apps/mobile/scripts/limrun-dev.sh: checks Limrun asset storage for an existing branch-scoped build first (fast path: install on a fresh simulator in seconds, no Xcode sandbox at all) and only builds when none exists or--rebuildis passed. A fresh build reapplies a required Xcode SPM sandbox fix (confirmed via a dedicated test that it does not survive snapshot restores or instance recreation, so it's cheap to reapply unconditionally rather than relying on persistence), forwards.envvalues via--env(.envis gitignored and never reaches Limrun's remote sync), and tunnels both Metro and the API port.apps/mobile/AGENTS.mddocuments all of the above so the next agent doesn't rediscover it.- Vendored
limrun-expo-development,limrun-ios-simulator,limrun-maestro-testing,limrun-xcodeskills vialim skills install(committed, matching how the repo already vendors other skills).limrun-android-emulatorandlimrun-gradleare deliberately not vendored - out of scope sinceapps/mobileis iOS-only today. EXPO_PUBLIC_SENTRY_DSN_MOBILEplaceholder added to.env.local.example: an unset/empty DSN causes@sentry/react-native's native init to SIGSEGV before any JS runs, which looks nothing like a Sentry problem. A real DSN is provisioned as an environment secret for sandboxes; local dev falls back to this inert placeholder.
Part 3: Mobile Simulator pane
Why
A pane analogous to the existing Desktop/VNC pane, so an agent or developer can see and interact with a mobile simulator inline instead of needing a second window/device - and critically, something that still works on a headless cloud sandbox where there's no local Xcode/Android SDK at all.
What / How
types.ts:MobilePaneDataadded to thePaneViewerDataunion.hotkeys/registry.ts+useWorkspaceHotkeys.ts:Cmd+Shift+M(Ctrl+Shift+Alt+Mon Windows/Linux) splits the active pane with a mobile pane. Not gated onisSandbox(unlike the Desktop/VNC hotkey) since it should also work on a real machine with local tooling.usePaneRegistry.tsx: registers themobilepane kind unconditionally, rendering<MobilePane />.packages/host-service/src/trpc/router/mobile/mobile.ts(new router):detectMobileBackend()pickslimrun(sandbox +LIM_API_KEY),local-ios(darwin +xcrun+ a booted simulator),local-android(adbpresent), ornone.limrunSessionmints/reuses a Limrun instance server-side via a direct REST call (the org-wideLIM_API_KEYnever reaches the renderer) and returns just the WebSocket URL + token<RemoteControl />needs.localSessionspawns/reuses@expo/serve-simfor iOS, or aSUPERSET_ANDROID_MIRROR_CMD-configured process for Android (no bundled default for Android - the npm screen-mirroring ecosystem there is fragmented and nothing has been run against real hardware yet; shipping a guess would overclaim confidence).MobilePane.tsx: renders Limrun's own<RemoteControl />(from the new@limrun/uidependency) directly for the cloud path, or embeds the local tool's browser preview UI in a<webview>for the local paths.
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Cloud rendering | Limrun's own <RemoteControl /> | It's a real React component with its own connecting/reconnecting UI; wrapping it beats reimplementing its streaming protocol |
| Local rendering | <webview> embedding the local tool's own browser UI | @expo/serve-sim (iOS) already ships a browser-embeddable preview; no need to speak SimulatorKit/IOSurface ourselves |
| Android local backend | Configurable via SUPERSET_ANDROID_MIRROR_CMD, no default | serve-sim has no Android equivalent (scrcpy renders to a native window); the right fork of ws-scrcpy-style tooling is untested |
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Cmd+Shift+M (mac) / Ctrl+Shift+Alt+M (Windows/Linux) | Split the current pane and open a Mobile Simulator |
Manual QA Checklist
Auth sync
- Desktop auto-signs-in as the real workspace creator on a fresh cloud sandbox (verified live via CDP - session +
activeOrganizationIdpresent) - Mobile auto-signs-in via the redeemed one-time token on a cloud sandbox, verified live on a real Limrun iOS simulator (twice, including through the final
limrun-dev.shscript) -
/one-time-token/verifyand/generateboth 404 withNODE_ENV=production;/get-sessionstill 200 (verified live) - Mobile local auto-sign-in (
setup.shpath, not cloud) - implemented and type-checks, not independently re-verified in this session beyond the cloud path's shared code path
Limrun tooling
-
limrun-dev.shcold-build path verified live (build, attach, tunnel, Metro, dev-client launch) -
limrun-dev.shfast path (existing asset, no Xcode sandbox) verified live (~11s) - SPM sandbox fix non-persistence across snapshot restore confirmed via a dedicated build→snapshot→delete→restore→check test
Mobile Simulator pane
-
detectMobileBackend()verified in all three live branches (sandbox+key, no-key sandbox, no-adb) - Limrun REST call (
limrunSession) verified live against the real API -
bun run typecheckpasses forapps/desktopandpackages/host-service - Not verified: the hotkey → pane UI flow was not click-tested end-to-end in an open workspace (no existing local workspace in this sandbox's dev DB to test against)
- Not verified:
local-iosbackend (@expo/serve-sim) - no Mac available in this cloud sandbox - Not verified:
local-androidbackend - no Android device/emulator available in this cloud sandbox, and no defaultSUPERSET_ANDROID_MIRROR_CMDis shipped
Testing
bun run typecheckforapps/desktopandpackages/host-service- passbunx @biomejs/biome check- pass, no outstanding lint issues- Live verification as described in Manual QA above (CDP screenshots for desktop/mobile auth; real Limrun sandboxes for the mobile tooling and pane backend)
- Did not get a clean full-repo
bun testrun in this sandbox due to a pre-existing, unrelated Bun engine segfault (confirmed viagit stashthat it reproduces onmaintoo, not introduced by this diff)
Known Limitations
- The Mobile Simulator pane's
local-iosandlocal-androidbackends are implemented but entirely unverified on real hardware - this sandbox is Linux-only. Plan is to verify these on a canary build once merged. - No default Android screen-mirroring tool is bundled;
SUPERSET_ANDROID_MIRROR_CMDmust be configured by whoever tests the local-Android path first. - The pane's hotkey → split → render flow itself was not click-tested in a live, populated workspace.
Follow-ups
- Verify
local-ios/local-androidpanes on real hardware and pick/pin a concrete Android mirroring tool. - Consider a platform picker for Limrun sessions once
apps/mobilesupports Android (today it's iOS-only, so the pane always requestsios).
Risks / Rollout
- Risk: auth-adjacent changes (new always-on
oneTimeTokenplugin, dev-only gated) and a new tRPC router with a direct external REST call (api.limrun.com) using an org-wide API key that stays server-side. - Rollout: no feature flag; ships to desktop/mobile/host-service together since they version in lockstep. The
oneTimeTokenendpoint is hard-gated toNODE_ENV === "development", so it is inert in any production build regardless of this PR shipping. - Rollback: standard revert; no data migrations or schema changes involved.
Summary by cubic
Makes cloud and local sandboxes immediately usable for desktop and mobile development: they auto-sign-in as the real workspace creator instead of the shared dev account, a Mobile Simulator pane (Cmd+Shift+M) mirrors the Desktop/VNC pane, and verifying apps/mobile from a cloud sandbox is now one command.
- Desktop cloud sandboxes get a better-auth session minted server-side for the creator, written through the machine-bound AES-256-GCM encryption desktop already uses (extracted to
packages/shared); the key derives from machine-id, so the token is minted fresh per sandbox, never copied. - Mobile redeems a better-auth
oneTimeTokenbaked into.envasEXPO_PUBLIC_DEV_ONE_TIME_TOKENon both cloud and local setups; the plugin is always registered but hard-gated toNODE_ENV=developmentvia ahooks.beforecheck that 404s/one-time-token/*in production (a conditional plugins array breaks better-auth's tuple type inference repo-wide). apps/mobile/scripts/limrun-dev.shfast-paths off a shared, branch-scoped Limrun build (seconds, no Xcode) when one exists, otherwise builds (reapplying a non-persistent Xcode SPM sandbox fix) and tunnels Metro and the API port;.envis forwarded via--envsince it never reaches Limrun's remote sync.- Vendored
limrun-expo-development,limrun-ios-simulator,limrun-maestro-testing, andlimrun-xcodeskills; Android equivalents are deliberately skipped sinceapps/mobileis iOS-only. EXPO_PUBLIC_SENTRY_DSN_MOBILEplaceholder added to.env.local.example: an unset DSN makes@sentry/react-nativenative init SIGSEGV before JS runs.- The pane's new strings are translated across all 16 locales (
check:i18n).
Rendering paths
- Sandbox with
LIM_API_KEY: a new host-servicemobiletRPC router mints/reuses a Limrun instance server-side (the org-wide key never reaches the renderer) and returns the WebSocket URL + token for Limrun's own<RemoteControl />. - Real machine: iOS uses
@expo/serve-sim, Android uses aSUPERSET_ANDROID_MIRROR_CMD-configured process — no bundled Android default since nothing is verified on real hardware. Both embed the tool's own browser UI in a<webview>.
Verification and risks
- Live-verified: desktop/cloud mobile auto-sign-in, cloud mobile via
limrun-dev.sh, the production 404-gate, Limrun cold-build and fast paths, and SPM-fix non-persistence. Not verified: local iOS/Android backends (Linux-only sandbox, no hardware) and the hotkey→pane flow end-to-end. - No feature flag, data migration, or schema change; ships together since desktop, mobile, and host-service version in lockstep, and the
oneTimeTokenendpoint is inert in production builds.
Summary by CodeRabbit
- New Features
- Open a mobile simulator alongside your workspace in a split pane using the new keyboard shortcut.
- Mobile previews can connect to available local or cloud simulators.
- Development sign-in can happen automatically when a valid one-time token is available.
- Documentation
- Added guides for preparing and running mobile apps, controlling cloud simulators, testing with Maestro, and using remote Xcode. These cover setup, app launch, simulator interaction, troubleshooting, and preview workflows.