OAuth callbacks across worktrees
OAuth providers require a fixed callback URL. You register https://myapp-oauth.prt.dev/auth/callback once and forget about it. A start hook points that alias at whichever branch you’re testing. No provider dashboard changes per branch.
The problem
Every OAuth provider (Google, GitHub, Stripe Connect) needs a redirect URI registered in its dashboard. With one branch, that’s easy: register https://myapp-main.prt.dev/auth/callback and move on.
With worktrees, each branch gets a unique URL (myapp-feature-auth.prt.dev, myapp-billing-v2.prt.dev, etc.). You can’t register a new redirect URI for every branch. You also can’t use localhost:3000 as a callback because multiple worktrees share the machine.
You need one stable URL that always reaches the branch you’re actively testing OAuth in.
The solution: a stable alias + start hooks
Treeline’s router alias system lets you create a name like myapp-oauth.prt.dev that points at any port. Start hooks (v0.36.0) register and remove the alias automatically when you start and stop the server. Whichever branch starts with --with oauth last owns the callback URL.
Register https://myapp-oauth.prt.dev/auth/callback in your provider dashboard once. Then use the hook to claim it per-session.
Step 1: Define the hook
In your project’s .treeline.yml, add a named start hook. The pre_start command registers the alias before the server boots; post_stop removes it when the supervisor exits.
project: myapp
env:
PORT: "{port}"
DATABASE_NAME: "{database}"
REDIS_URL: "{redis_url}"
OAUTH_CALLBACK_HOST: "https://myapp-oauth.{router_domain}"
SESSION_COOKIE_DOMAIN: ".{router_domain}"
APPLICATION_HOST: "localhost:{port}"
hooks:
oauth:
pre_start: "gtl serve alias myapp-oauth {port}"
post_stop: "gtl serve alias --remove myapp-oauth"
The hook uses {port} interpolation. Whatever port Treeline allocated to this worktree is substituted at runtime. When the server stops (Ctrl+C), the alias is cleaned up so it doesn’t point at a dead port.
Step 2: Configure your app
Your application needs to know about the callback host and accept requests from the router domain. Here’s a Rails example. The same pattern works in any framework.
# Point OAuth callbacks at the stable alias URL
OmniAuth.config.full_host = ENV["OAUTH_CALLBACK_HOST"] # Share cookies across all *.prt.dev subdomains
Rails.application.config.session_store :cookie_store,
key: "_myapp_session",
domain: ENV["SESSION_COOKIE_DOMAIN"].presence # Allow requests from any *.prt.dev subdomain
config.hosts << ".#{ENV['SESSION_COOKIE_DOMAIN']}" # Trust X-Forwarded-Proto from the local router
Rails.application.config.middleware.insert_before 0,
Rack::SslEnforcer, only_hosts: /\.prt\.dev\z/,
redirect_to: ->(req) { req.url.sub("http://", "https://") }
The key insight: {router_domain} resolves to prt.dev at setup time. The session cookie is scoped to .prt.dev, so the user stays logged in when the browser redirects from myapp-oauth.prt.dev back to myapp-feature-auth.prt.dev. Treeline’s router sets X-Forwarded-Proto: https so the backend generates correct HTTPS redirect URIs.
Step 3: Start with the hook
$ gtl start --with oauth
[hook:oauth] Running pre_start: gtl serve alias myapp-oauth 3010
Alias myapp-oauth.prt.dev → :3010
[supervisor] Starting bin/dev...
▸ Rails booting on :3010
Now https://myapp-oauth.prt.dev routes to this worktree’s server. When the provider redirects to https://myapp-oauth.prt.dev/auth/google/callback, Rails receives the request on the correct branch.
When you Ctrl+C the supervisor, the post_stop hook removes the alias:
^C
[supervisor] Shutting down...
[hook:oauth] Running post_stop: gtl serve alias --remove myapp-oauth
Removed alias "myapp-oauth".
How it works
| Piece | Role |
|---|---|
| myapp-oauth.prt.dev | Stable alias registered in the OAuth provider. Never changes. |
pre_start hook | Points the alias at this worktree’s allocated port |
post_stop hook | Removes the alias so it doesn’t point at a dead port |
{router_domain} | Env token that resolves to prt.dev, used for cookie domain and host matching |
--with oauth | Activates the hook for this session. Only the branch that runs it gets the alias. |
Why this is worktree provisioning
This isn’t just port allocation. The .treeline.yml file declares the full environment a worktree needs to function: ports, databases, routing aliases, session boundaries, SSL forwarding, and OAuth integration. When a new developer clones the repo and runs gtl start --with oauth, everything is provisioned. No README steps to follow, no manual provider configuration, no “ask Sarah how she set up her local OAuth.”
The hook is opt-in (--with oauth) because only one branch should own the callback URL at a time. If you want it to activate automatically on every gtl start, add auto: true to the hook config.
Adapting for other frameworks
The pattern is framework-agnostic. The three things any app needs:
- Callback host override. Point your OAuth library at
OAUTH_CALLBACK_HOST instead of inferring from the request. - Cookie domain. Scope session cookies to
.prt.dev so the login survives the redirect from the alias URL back to the branch URL. - Host allowlist. Accept requests from
*.prt.dev subdomains (Rails config.hosts, Django ALLOWED_HOSTS, Express CORS origins).
Read next
- Named URLs for local services: how
gtl serve alias works outside of hooks - Why port 3000 stays free: the proxy facade for integrations that demand a fixed port
- Docs: Hooks guide: full reference for
pre_start, post_stop, and auto - Networking feature page: how serve, proxy, tunnel, and share fit together