Make dev servers shareable from other devices#4489
Conversation
Browser dev is now single-origin: Vite proxies /api, /ws, /oauth, and /.well-known to the backend, and dev/dev:web no longer bake VITE_HTTP_URL/VITE_WS_URL into the bundle. Absolute localhost URLs in the bundle sent any remote browser to its own machine, which no amount of external path mapping could fix. - `dev --share` publishes the web port on the tailnet and logs a pairing URL already built against that origin - `dev:pair` mints a pairing URL for a running server, resolving port and state dir from server-runtime.json - Ports derive from the worktree path, so each worktree is stable across restarts; the availability probe is loopback-only so `tailscale serve` can't push the port - Web session cookies are port-scoped like desktop's, ending the cross-instance "Invalid session token signature" loop - Dev startup pairing tokens last 24h instead of 5m; user-issued links keep the 5m default Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis change adds deterministic worktree-based dev ports, tailnet sharing, live-server pairing URL recovery, single-origin browser proxying, port-scoped session cookies, and updated development documentation and tests. ChangesDev server sharing and pairing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DevRunner as dev-runner
participant Tailscale
participant Vite
participant Browser
participant Server
DevRunner->>Tailscale: publish local web port with tailscale serve
Tailscale-->>DevRunner: return tailnet host and HTTPS URL
DevRunner->>Vite: configure shared origin and allowed origins
Browser->>Vite: request application and proxied routes
Vite->>Server: proxy API, OAuth, websocket, and well-known requests
Server-->>Browser: return application responses
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review New feature introducing Tailscale dev server sharing with behavioral changes to session cookie scoping, dev token TTL (5min→24hrs), and network exposure patterns. The auth-adjacent changes and new integration warrant human review. You can customize Macroscope's approvability policy. Learn more. |
- `auth pairing url` resolved the state directory with the same flag heuristic the command exists to hide, so without --dev-url it read `userdata` while `bun run dev` writes to `dev`. It then minted into the wrong database and printed a URL the live server rejects with `invalid_credential`. It now searches both state directories and issues against whichever one the running server actually uses. - A server killed with SIGKILL leaves `server-runtime.json` behind, and that was taken as proof it was still up. Verify the recorded pid is alive before minting. - `--dry-run --share` replaced and then tore down the port's existing tailscale mapping. Dry run now returns before the share block. deriveServerPaths takes an optional explicit stateDir for callers that have already located a running server and must target it exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`findLiveServerRuntimeState` checked the configured state path first, which without --dev-url resolves to `userdata`. With both a userdata and a dev server running under the same base directory, `auth pairing url` would mint against the userdata server and print its origin — not the dev server this command exists to pair with. Rank candidates by the `devUrl` field instead, which only a server fronted by a web dev server records. Falls back to any live server when no dev server is running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
apps/server/src/http.ts (1)
55-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRoute
T3CODE_DEV_ALLOWED_ORIGINSthroughServerConfiginstead of rawprocess.env.This function already yields
ServerConfig.ServerConfigfordevOrigin; readingprocess.env.T3CODE_DEV_ALLOWED_ORIGINSdirectly alongside it breaks the config-injection pattern used everywhere else (e.g.devUrl,tailscaleServeEnabled), making this harder to override in tests that injectServerConfigvia layers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/http.ts` around lines 55 - 76, Update browserApiCorsLayer to read T3CODE_DEV_ALLOWED_ORIGINS from the yielded ServerConfig.ServerConfig instance instead of process.env. Add or use the corresponding ServerConfig field, preserving the existing trimming, filtering, and allowedOrigins behavior so injected configuration layers can override it in tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.agents/skills/test-t3-app/SKILL.md:
- Around line 82-85: Update the pairing-URL guidance in the skill instructions
to explicitly allow sharing the one-time URL in the response when the user
explicitly requests access, while prohibiting its inclusion in screenshots,
committed files, or durable logs. Reconcile this exception with the existing
prohibition so the intended behavior is unambiguous.
In `@apps/server/src/cli/auth.ts`:
- Around line 142-228: 添加针对 findLiveServerRuntimeState 和 pairingUrlCommand
的聚焦测试,覆盖无存活候选、从 dev 与 userdata 候选中选择正确运行时状态,以及 devUrl 存在时优先使用它、否则回退到 origin
的分支。将测试放在现有 CLI/服务器运行时状态测试约定的位置,并确保相关测试命令会执行这些用例。
In `@docs/reference/scripts.md`:
- Around line 47-50: Qualify the worktree port-stability wording in
docs/reference/scripts.md lines 47-50: replace the claim that worktrees do not
collide with siblings, state that the hash provides a stable preferred pair when
available, and emphasize using the resolved values printed on the “[dev-runner]”
line. Apply the same “stable when available, not guaranteed unique” wording to
the worktree port description in .agents/skills/test-t3-app/SKILL.md lines
21-24.
In `@scripts/lib/dev-share.ts`:
- Around line 148-173: Update shareDevServer so a failed new serve does not
silently discard an existing working mapping: either defer unshareDevServer
until the replacement serve is ready, or preserve and restore the prior mapping
when serve fails. If restoration is not possible, include that the previous
mapping was removed in the DevShareError detail.
---
Nitpick comments:
In `@apps/server/src/http.ts`:
- Around line 55-76: Update browserApiCorsLayer to read
T3CODE_DEV_ALLOWED_ORIGINS from the yielded ServerConfig.ServerConfig instance
instead of process.env. Add or use the corresponding ServerConfig field,
preserving the existing trimming, filtering, and allowedOrigins behavior so
injected configuration layers can override it in tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 66754ed0-b428-4310-a2bc-a7839a1977b1
📒 Files selected for processing (20)
.agents/skills/test-t3-app/SKILL.mdAGENTS.mdapps/server/src/auth/EnvironmentAuth.test.tsapps/server/src/auth/EnvironmentAuth.tsapps/server/src/auth/EnvironmentAuthPolicy.test.tsapps/server/src/auth/EnvironmentAuthPolicy.tsapps/server/src/auth/PairingGrantStore.tsapps/server/src/auth/SessionStore.tsapps/server/src/auth/utils.tsapps/server/src/cli/auth.tsapps/server/src/config.tsapps/server/src/http.tsapps/server/src/serverRuntimeState.test.tsapps/server/src/serverRuntimeState.tsapps/web/vite.config.tsdocs/reference/scripts.mdpackage.jsonscripts/dev-runner.test.tsscripts/dev-runner.tsscripts/lib/dev-share.ts
- The skill told agents both to never put a pairing URL in a response and to hand the shared one to the user. State the rule and its single exception explicitly: give the URL to the person who asked for access, never put it anywhere durable. - A failed `tailscale serve` left the port unshared, because the stale- mapping clear had already run. The clear is still required (old mappings carry path routes that serving "/" would not replace), so say so in the error instead of letting an operator assume the previous mapping survived. - Worktree-derived ports were described as collision-free. They are a preferred offset, not a guarantee — hashes can collide and occupied ports shift both values. Qualified in AGENTS.md, the skill, and docs/reference/scripts.md. CodeRabbit also asked for tests around the pairing-url command; those landed in 0555604, which it had not yet seen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 827469b. Configure here.
`unshareDevServer` applied Effect.ignore, so a failed removal was indistinguishable from a successful one. `shareDevServer` then served over routes it had not removed: stale path entries from the older `tsdev t3` layout (/ws, /api pointing at a separate backend port) would survive, producing a URL that loads while its API calls resolve to a dead port. The serve-failed message also asserted the port was cleared without knowing it. It now reports whether the port is clear, and sharing refuses when it is not. `tailscale serve … off` exits nonzero with "handler does not exist" when nothing was mapped — the normal first-share case — so that counts as cleared; anything else does not. The dev-runner finalizer warns, with the command to run, when cleanup does not take. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Sharing a dev server so it can be opened from a phone or another laptop took a full session of workarounds and still ended up hand-launching processes. This fixes the causes.
Browser dev is now single-origin: Vite proxies
/api,/ws,/oauth, and/.well-knownto the backend, anddev/dev:webno longer setVITE_HTTP_URL/VITE_WS_URL. Those were compiled into the bundle, so any browser not on the dev machine dialed its own localhost — no amount of external path mapping could fix it.dev:desktopstill sets them; its renderer talks to the backend directly.dev --share(bun run dev:share) publishes the web port on the tailnet and logs a pairing URL already built against that origin. The mapping is removed on exit, and re-running clears a stale one.bun run dev:pairmints a pairing URL for an already-running server, resolving port and state dir fromserver-runtime.json— no flags to get wrong. The previously documentedauth pairing createsnippet passed--base-dir, which points the CLI at a different SQLite file than the server reads.tailscale servepush the dev server to another port mid-setup.t3_session_<port>) like desktop's. Cookies ignore ports, so servers sharing a hostname clobbered each other into a permanentInvalid session token signatureloop.allowedHostscovers.ts.net;T3CODE_DEV_ALLOWED_HOSTS/T3CODE_DEV_ALLOWED_ORIGINShandle anything else.Testing
Ran over a real tailnet:
/,/pair,/.well-known/t3/environment, and/api/auth/sessionall 200;/wsupgrade reaches the backend; token exchange returnedauthenticated: truewith cookiet3_session_<port>. Confirmed in SQLite that the dev startup token gets 24h while CLI-minted tokens stay at 5m, and that Ctrl+C removes the tailnet mapping.Typecheck clean; 3,282 tests pass across
t3,@t3tools/web, andscripts.Note
Not addressed here:
vp's child processes can outlive the dev-runner parent on interrupt and keep holding ports. Pre-existing, and separate from this change.🤖 Generated with Claude Code
Note
Medium Risk
Touches session cookie naming and auth TTLs (breaking for existing
t3_sessioncookies on web dev), plus dev-runner env and Tailscale side effects; scope is local dev tooling with tests, not production auth paths.Overview
Makes browser dev servers usable from phones and other machines on the tailnet without hand-mapping ports or baking
localhostinto the client bundle.Single-origin web dev:
dev/dev:webno longer setVITE_HTTP_URL/VITE_WS_URL; Vite proxies/api,/ws,/oauth, and/.well-knownto the backend viaT3CODE_PORT. HMR stays origin-derived except indev:desktop. Vite allows.ts.nethosts; the server accepts extra dev origins viaT3CODE_DEV_ALLOWED_ORIGINSand returns 404 (not redirect loops) for unmatched proxied API paths whendevUrlis set.Tailnet sharing:
--share/bun run dev:sharepublishes the web port withtailscale serve, setsVITE_DEV_SERVER_URLto the HTTPS tailnet URL, and tears the mapping down on exit. Port probes are loopback-only so an existing tailscale binding does not bump ports mid-share.Pairing & runtime discovery:
bun run dev:pair(auth pairing url) finds a live server viaserver-runtime.json(bothdev/userdata, PID check), issues against the correct state dir, and builds the link from recordeddevUrl. Dev administrative startup tokens get a 24h TTL whendevUrlis configured; user-issued links stay at 5m.Multi-instance auth: Session cookies are always
t3_session_<port>so concurrent servers on one hostname do not overwrite each other’s sessions.Ports: Worktree path hashing picks a stable preferred port offset when
T3CODE_PORT_OFFSET/T3CODE_DEV_INSTANCEare unset.Docs and agent skills (
AGENTS.md,test-t3-app) describe share/pair, single-origin rules, and updated troubleshooting.Reviewed by Cursor Bugbot for commit 2682f8a. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Tailscale-based dev server sharing with
dev:shareanddev:paircommandsbun run dev:shareto publish the local dev web server over HTTPS on tailnet viatailscale serve, andbun run dev:pairto print a ready-to-open pairing URL for the running server./oauthand/ws, and leaves HMR host unset so it derives from page origin — enabling remote access from shared origins.t3_session_13773) across all modes, removing the previous mode-based branching inresolveSessionCookieName.VITE_HTTP_URL/VITE_WS_URLinto the environment; the client proxies through Vite instead, which breaks direct localhost backend access in dev when not using the proxy.Macroscope summarized 2682f8a.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation