Why port 3000 stays free
OAuth providers, Stripe webhooks, Mapbox API keys, and CORS allowlists are all registered to localhost:3000. That works when you have one copy of the app. It breaks the moment you run two.
The problem
Git worktrees let you work on multiple branches at the same time, each in its own directory. But each copy of the app needs its own port — branch A on 3000, branch B on 3010, branch C on 3020. The moment branch B starts on port 3010:
- OAuth redirects are configured for
localhost:3000/auth/callback— the provider rejects the redirect from:3010 - Mapbox / Google Maps validate the page origin — tile requests from
localhost:3010are denied - Stripe webhooks point at
localhost:3000/webhooks— events never reach the branch B server - CORS policies allowlist
localhost:3000— API calls from:3010are blocked
You’re forced to choose: run on the “right” port and stop all other branches, or reconfigure every third-party dashboard for every branch. Neither scales.
The design decision
Treeline deliberately never allocates port 3000 — or any common framework default (4000, 5000, 8000, 8080). Worktree servers bind to ports in a managed range that the developer never needs to type. Port 3000 stays empty so the proxy can claim it.
gtl proxy 3000 sits on the port that third-party services expect and routes traffic to whichever worktree you’re working on. The external world always sees localhost:3000. You rotate what’s behind it.
$ gtl proxy 3000
Proxying :3000 → :4010 (feature-auth)
# OAuth callback → :4010 ✓
# Stripe webhook → :4010 ✓
# Mapbox origin → :4010 ✓
# CORS passes → :4010 ✓
Two layers, one config
The proxy handles what’s external. Branch-named URLs handle what’s local. Both run simultaneously:
| Audience | URL | How |
|---|---|---|
| Your browser | myapp-feature.prt.dev | gtl serve |
| OAuth / Stripe / Maps / CORS | localhost:3000 | gtl proxy |
| Public internet (webhooks, CI) | myapp-feature.example.dev | gtl tunnel |
| A colleague or client | Disposable token-gated URL | gtl share |
What the developer sees
# Create a worktree — treeline allocates port 4010
$ gtl new feature/oauth-refactor
Allocated port 4010, cloned database, wrote .env
# Start the server — runs on 4010, not 3000
$ gtl start
==> https://myapp-feature-oauth-refactor.prt.dev
# Route port 3000 to this worktree
$ gtl proxy 3000
Proxying :3000 → :4010
No port numbers in the browser. No OAuth reconfiguration. No Stripe dashboard changes. The developer works on any branch, and the external world sees the same origin it always has.
Read next
- Networking feature page — deep dive on all four commands:
serve,proxy,tunnel, andshare - Docs: networking — config options, port forwarding rules, and Safari workarounds
- Multi-repo — when the problem is cross-project service resolution, not external URLs