Use cases

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.

.treeline.yml
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.

config/initializers/omniauth.rb
# Point OAuth callbacks at the stable alias URL
OmniAuth.config.full_host = ENV["OAUTH_CALLBACK_HOST"]
config/initializers/session_store.rb
# Share cookies across all *.prt.dev subdomains
Rails.application.config.session_store :cookie_store,
  key: "_myapp_session",
  domain: ENV["SESSION_COOKIE_DOMAIN"].presence
config/environments/development.rb
# Allow requests from any *.prt.dev subdomain
config.hosts << ".#{ENV['SESSION_COOKIE_DOMAIN']}"
config/initializers/forwarded_ssl.rb
# 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:

  1. Callback host override. Point your OAuth library at OAUTH_CALLBACK_HOST instead of inferring from the request.
  2. Cookie domain. Scope session cookies to .prt.dev so the login survives the redirect from the alias URL back to the branch URL.
  3. Host allowlist. Accept requests from *.prt.dev subdomains (Rails config.hosts, Django ALLOWED_HOSTS, Express CORS origins).

Read next

All use cases