Today Platform Web — Dev Docs
Workflow

Parallel worktrees

Run multiple branches in parallel via `git worktree add`. What env files to mirror, the cookie-jar caveat, and the future per-worktree hostname plan.

The repo expects you to use git worktrees for parallel branches. Each worktree is a complete checkout pointing at a different branch, with shared .git/ storage. The .worktrees/ directory at the repo root is gitignored — that's the conventional location.

Creating a worktree

git fetch origin dev
git worktree add -b pr-x/topic .worktrees/pr-x-topic origin/dev
cd .worktrees/pr-x-topic

You now have a complete checkout at .worktrees/pr-x-topic/ on a new branch pr-x/topic tracking origin/dev. Run pnpm install once and the worktree is ready (pnpm shares the global store, so it's fast).

Env files to mirror

Fresh worktree checkouts have an empty working tree for every gitignored file. The next-app per-package .env*.local files plus the root .env.cloud are gitignored but required for the dev server and cloud-credential scripts to work. Mirror them from your main checkout the first time:

SRC="/path/to/your/main/checkout"
for f in \
  .env.cloud \
  apps/web/.env.development.local \
  apps/admin/.env.development.local \
  apps/onboarding/.env.local \
  design-system/docs/.env.local; do
  [ -f "$SRC/$f" ] && cp "$SRC/$f" "./$f" && echo "copied $f"
done

What each one is:

FilePurpose
.env.cloudClaude Code cloud-session env vars (VERCEL_TOKEN, AWS, GH PAT, E2E secrets, Slack tokens, bypass token). Template: .env.cloud.example
apps/web/.env.development.localOIDC client_id + secret for web's dev profile. Without it, auth init fails
apps/admin/.env.development.localSame shape for admin
apps/onboarding/.env.localVite-style overrides (VITE_API_URL, etc.)
design-system/docs/.env.localDocs-site overrides

pnpm cloud:bootstrap regenerates the two Next apps' .env.development.local via vercel pull if VERCEL_TOKEN is in .env.cloud — useful when the upstream values change.

Do not run pnpm dev:local or pnpm dev:remote in a fresh worktree before mirroring — they delete or overwrite .env.development.local and you'll lose your OIDC creds.

Per-worktree dev ports — only apps/web is automatic today

scripts/dev-server.mjs scans 4060-4069 for the first free port. So if worktree A is running apps/web's pnpm dev on :4060, worktree B's pnpm dev lands on :4061 without configuration.

apps/admin, apps/onboarding, and apps/dev-docs do not use the scanner. They are hardcoded:

  • apps/adminnext dev --turbopack -p 4061
  • apps/onboardingvite --port 5174 (Vite uses strictPort: true)
  • apps/dev-docsnext dev --turbopack -p 4070

So two worktrees can run their apps/web side by side, but only one worktree at a time can run any of the other three. Generalizing the scanner to admin

  • dev-docs is on the follow-up list; apps/onboarding's native-WebView behavior depends on a fixed port and is intentionally out of scope.

Two worktrees on localhost:<different ports> share one cookie jar within a single browser profile (RFC 6265 — port is not part of cookie scope). Logging into one replaces the other's session in that profile.

Workarounds:

  • Automation: Playwright browser.newContext() and agent-browser --session <name> each isolate their own cookie jar. Verified by the two-worktree simultaneous-login E2E in PR #656.
  • Humans: use a separate Chrome profile per worktree when you need two worktrees logged in at the same time. Clunky but it's the only workaround at the port layer today.

Future direction — per-worktree hostnames

There's a planned (not yet built) scheme to give each worktree its own hostname instead of port. Schema:

<project>-<feature>.localhost:<port>

Examples: today-web-localhost-bff.localhost:4060, today-web-pr-ca-dev-docs.localhost:4061. *.localhost resolves to loopback in Chrome and Firefox without /etc/hosts edits, so a distinct hostname = distinct cookie jar = true same-profile parallel login.

The constraint to know: keep the name single-label (one dot before .localhost). A multi-level form like today.dev-docs.localhost is unreliable behind DNS-intercepting proxies (Surge, Proxyman) which can route depth-greater-than-1 .localhost names to the real internet. The project name is therefore folded into the single label with a dash.

Tracked in the team-lead memory feedback_dev_preview_url_scheme.

Cleanup

# From the main checkout
git worktree list                          # see all live worktrees
git worktree remove .worktrees/pr-x-topic  # delete the checkout + branch metadata
git worktree prune                         # clean up stale worktree records

Don't delete .worktrees/<name>/ manually with rm -rf — git keeps a record of every worktree under .git/worktrees/ that gets stale.

Local state noise to ignore

git status in the main checkout sometimes shows:

  • D .claude/scheduled_tasks.lock — Claude Code session state, ephemeral
  • ?? .claude/worktrees/ — Claude Code's own worktree registry

Both are local agent state. Don't stage them.

On this page