Crowi dev ports / portal / tailscale
pnpm dev auto-detects the worktree and assigns each one a deterministic
4-port block (stride 10): api = anchor, web = anchor+1, site = anchor+2,
proxy = anchor+3. main is pinned to anchor 4301 (today's ports, no
migration); every other worktree gets the next free block starting at 4310,
recorded in ~/.crowi-dev-ports.json (outside the repo, shared across every
worktree checkout) so the same worktree always gets the same anchor. Pin one
explicitly with pnpm dev --anchor 4350. Fixed-port tooling must stay outside
this 4300-4999 band or it clashes with a worktree's block — e.g. the e2e
servers sit just below at 4290/4291 (packages/e2e/src/config.ts).
- The proxy (
anchor+3) is the canonical dev entry point, not the raw web port.pnpm devfronts api + web + the collab/presence/notifications WebSocket namespaces behind one same-origin reverse proxy (Caddy if installed, otherwise a zero-dep node fallback — seescripts/dev-caddy.mjs), the same routing table as the prod front proxy (Caddyfile). This is what lets realtime editing work at all: Next'srewrites()is HTTP-only and can't forward a WSupgrade, so opening the web port directly skips the proxy and collab/presence/notifications won't connect (see the doc comment inpackages/web/src/lib/resolve-ws-url.ts). - tailscale serve publishes only the proxy port (
tailscale serve --bg --https=<anchor+3> localhost:<anchor+3>) so an iPhone/other Mac on the same tailnet can openhttps://<your-machine>.<tailnet>.ts.net:<anchor+3>and get full realtime editing without restartingpnpm dev. Requires tailnet HTTPS/MagicDNS enabled; missing/not-logged-intailscalejust warns and continues (localhost still works). Ctrl-C / closing a worktree only turns off that worktree's serve (--https=<anchor+3> off— nevertailscale serve reset, which would also drop every other worktree's proxy). - The dev portal is a read-only dashboard on a fixed
:4300listing every live worktree (fromgit worktree list) with its up/down status (proxy port probe), reachable proxy URLs (localhost + this host's LAN/tailscale IPs + the tailscale MagicDNS URL, rendered as a mobile-friendly card layout), and DB (shared vs. isolated). It's a separate long-lived process so restarting one worktree doesn't take the portal down for the others, and self-GCs registry entries for worktrees that no longer exist. The main worktree'spnpm devauto-starts it (main is the always-around home base; feature worktrees rely on main's). Run it standalone withpnpm dev:portal, or opt main out withCROWI_DEV_NO_PORTAL=1. Both the portal and each worktree's proxy bind0.0.0.0, so they're reachable by IP (http://<ip>:4300/http://<ip>:<anchor+3>) from a phone on the tailnet or LAN even without thetailscaleCLI (dev-only; also LAN-exposed, an accepted tradeoff). - DB isolation is opt-in and mongo-only (redis/ES always stay shared —
redis's URL parser ignores the db-number path segment, and both are
ephemeral state). Add
dev.local.jsonat the worktree root (gitignored):{ "isolateDb": true }, or pass--isolate-db; the mongo DB name becomescrowi_<key>instead of the shared one.pnpm migrate apply(the only destructive migrate subcommand —--dry-run/plan/statusare exempt) refuses to run from a non-main worktree against the shared DB unless you isolate it, pass--yes, or setCROWI_MIGRATE_FORCE=1(non-interactive environments fail closed instead of hanging on a prompt). - Implementation lives in
scripts/dev-ports.mjs(registry/lock/key normalization),scripts/dev-caddy.mjs(proxy config generation + fallback),scripts/dev-portal/(the dashboard), and thescripts/dev.mjs/scripts/migrate.mjsextensions that wire them together.


