Skip to content

Make dev servers shareable from other devices#4489

Open
t3dotgg wants to merge 5 commits into
mainfrom
t3code/simplify-dev-server-hosting
Open

Make dev servers shareable from other devices#4489
t3dotgg wants to merge 5 commits into
mainfrom
t3code/simplify-dev-server-hosting

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member

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-known to the backend, and dev/dev:web no longer set VITE_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:desktop still 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:pair mints a pairing URL for an already-running server, resolving port and state dir from server-runtime.json — no flags to get wrong. The previously documented auth pairing create snippet passed --base-dir, which points the CLI at a different SQLite file than the server reads.
  • Ports derive from the worktree path, so each worktree is stable across restarts. The availability probe is loopback-only; probing wildcards let a live tailscale serve push the dev server to another port mid-setup.
  • Web session cookies are port-scoped (t3_session_<port>) like desktop's. Cookies ignore ports, so servers sharing a hostname clobbered each other into a permanent Invalid session token signature loop.
  • Dev startup pairing tokens last 24h instead of 5m. User-issued links keep the 5m default.
  • Vite allowedHosts covers .ts.net; T3CODE_DEV_ALLOWED_HOSTS / T3CODE_DEV_ALLOWED_ORIGINS handle anything else.

Testing

Ran over a real tailnet: /, /pair, /.well-known/t3/environment, and /api/auth/session all 200; /ws upgrade reaches the backend; token exchange returned authenticated: true with cookie t3_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, and scripts.

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_session cookies 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 localhost into the client bundle.

Single-origin web dev: dev/dev:web no longer set VITE_HTTP_URL/VITE_WS_URL; Vite proxies /api, /ws, /oauth, and /.well-known to the backend via T3CODE_PORT. HMR stays origin-derived except in dev:desktop. Vite allows .ts.net hosts; the server accepts extra dev origins via T3CODE_DEV_ALLOWED_ORIGINS and returns 404 (not redirect loops) for unmatched proxied API paths when devUrl is set.

Tailnet sharing: --share / bun run dev:share publishes the web port with tailscale serve, sets VITE_DEV_SERVER_URL to 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 via server-runtime.json (both dev/userdata, PID check), issues against the correct state dir, and builds the link from recorded devUrl. Dev administrative startup tokens get a 24h TTL when devUrl is 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_INSTANCE are 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:share and dev:pair commands

  • Adds bun run dev:share to publish the local dev web server over HTTPS on tailnet via tailscale serve, and bun run dev:pair to print a ready-to-open pairing URL for the running server.
  • Introduces dev-share.ts with structured error handling for share/unshare operations, tailnet host detection, and cleanup on exit.
  • Vite now accepts additional allowed hosts, proxies /oauth and /ws, and leaves HMR host unset so it derives from page origin — enabling remote access from shared origins.
  • Session cookie names are now always port-scoped (e.g. t3_session_13773) across all modes, removing the previous mode-based branching in resolveSessionCookieName.
  • Admin bootstrap one-time tokens issued in dev environments now default to a 24-hour TTL instead of 5 minutes.
  • Behavioral Change: browser dev no longer bakes VITE_HTTP_URL/VITE_WS_URL into 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

    • Added optional sharing of local development servers through secure tailnet URLs.
    • Added commands to generate fresh pairing URLs for running development servers.
    • Improved support for running multiple development instances with predictable port assignment.
    • Added proxy support for authentication, WebSocket, and related development routes.
  • Bug Fixes

    • Prevented session cookies from conflicting between servers running on different ports.
    • Improved handling of stale server state and shared-server host validation.
  • Documentation

    • Expanded development setup, sharing, pairing, troubleshooting, and proxy guidance.

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>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Dev server sharing and pairing

Layer / File(s) Summary
Authentication and token contracts
apps/server/src/auth/*
Session cookies now include the server port, and administrative bootstrap tokens use a shared subject constant with a 24-hour dev-server TTL.
Live runtime state and pairing URL command
apps/server/src/serverRuntimeState.ts, apps/server/src/config.ts, apps/server/src/cli/auth.ts, package.json
Runtime state records the dev URL, validates process liveness, and supports generating pairing URLs from an already-running server.
Tailnet sharing and dev-runner lifecycle
scripts/dev-runner.ts, scripts/lib/dev-share.ts, scripts/dev-runner.test.ts
The runner derives worktree-specific ports, adds --share, publishes the web port through Tailscale, and cleans up the mapping on exit.
Single-origin browser proxying
apps/web/vite.config.ts, apps/server/src/http.ts, scripts/dev-runner.ts
Browser dev leaves absolute backend URLs unset, proxies API and websocket routes through Vite, and applies host and origin allowlists.
Development workflow guidance
.agents/skills/test-t3-app/SKILL.md, AGENTS.md, docs/reference/scripts.md
Documentation explains derived ports, sharing, pairing recovery, proxy constraints, and troubleshooting.

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
Loading

Possibly related PRs

  • pingdotgg/t3code#20: Related deterministic multi-instance dev-runner port derivation and development script environment wiring.

Suggested reviewers: juliusmarminge

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: making dev servers shareable from other devices.
Description check ✅ Passed The description covers what changed, why, and testing, though it doesn't follow the template headings or include the checklist section.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/simplify-dev-server-hosting

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 24, 2026
Comment thread apps/server/src/cli/auth.ts Outdated
Comment thread scripts/dev-runner.ts
Comment thread apps/server/src/cli/auth.ts
@macroscopeapp

macroscopeapp Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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>
Comment thread apps/server/src/cli/auth.ts Outdated
`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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/server/src/http.ts (1)

55-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Route T3CODE_DEV_ALLOWED_ORIGINS through ServerConfig instead of raw process.env.

This function already yields ServerConfig.ServerConfig for devOrigin; reading process.env.T3CODE_DEV_ALLOWED_ORIGINS directly alongside it breaks the config-injection pattern used everywhere else (e.g. devUrl, tailscaleServeEnabled), making this harder to override in tests that inject ServerConfig via 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

📥 Commits

Reviewing files that changed from the base of the PR and between 38cfc25 and 36fe3b3.

📒 Files selected for processing (20)
  • .agents/skills/test-t3-app/SKILL.md
  • AGENTS.md
  • apps/server/src/auth/EnvironmentAuth.test.ts
  • apps/server/src/auth/EnvironmentAuth.ts
  • apps/server/src/auth/EnvironmentAuthPolicy.test.ts
  • apps/server/src/auth/EnvironmentAuthPolicy.ts
  • apps/server/src/auth/PairingGrantStore.ts
  • apps/server/src/auth/SessionStore.ts
  • apps/server/src/auth/utils.ts
  • apps/server/src/cli/auth.ts
  • apps/server/src/config.ts
  • apps/server/src/http.ts
  • apps/server/src/serverRuntimeState.test.ts
  • apps/server/src/serverRuntimeState.ts
  • apps/web/vite.config.ts
  • docs/reference/scripts.md
  • package.json
  • scripts/dev-runner.test.ts
  • scripts/dev-runner.ts
  • scripts/lib/dev-share.ts

Comment thread .agents/skills/test-t3-app/SKILL.md
Comment thread apps/server/src/cli/auth.ts Outdated
Comment thread docs/reference/scripts.md Outdated
Comment thread scripts/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>
Comment thread scripts/lib/dev-share.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread scripts/lib/dev-share.ts
`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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant