The Backend.AI WebUI dev server runs behind Portless, which assigns each branch a stable *.localhost subdomain. No more port offsets, no more port conflicts between worktrees.
portless is bundled as a devDependency. After pnpm install, pnpm run dev invokes the project-local Portless binary — no npm install -g portless needed.
Safari (and some corporate network setups) need /etc/hosts entries for *.localhost resolution:
sudo pnpm exec portless hosts sync # one-timeIn two terminals:
# Terminal 1 — TypeScript watch + Relay watch + CRA dev server (via Portless)
pnpm run dev
# Terminal 2 — WebSocket proxy (plain, fixed port 5050)
pnpm run wsproxypnpm run dev automatically starts the Portless daemon on port 1355 (idempotent — no-op if already running) and prints the assigned URL on startup, for example:
-> https://fr-2701.localhost:1355
Open that URL in your browser. Portless 0.10+ serves HTTPS/2 by default; the local CA is auto-installed in your system keychain on first start (no sudo on macOS).
Portless's default daemon port is 443, which requires sudo. dev.mjs starts the daemon with -p 1355 so dev never prompts for a password.
To pin a different daemon port on a machine — for example, when another Portless daemon is already running — export PORTLESS_PORT in your shell rc:
export PORTLESS_PORT=1356When PORTLESS_PORT is set, dev.mjs skips its explicit -p flag entirely and lets Portless read the env var itself — both for the daemon start and for subsequent portless client calls (portless run, portless list, …) — so daemon and clients stay aligned via a single source of truth.
The remaining *.localhost:1355 URLs and portless proxy start -p 1355 examples in this document describe the default. When PORTLESS_PORT is set, substitute its value in those URLs and commands.
scripts/dev.mjs picks the Portless app name as follows:
- If the current git branch matches
FR-XXXX(case-insensitive —fr-XXXX,feat/FR-XXXX-...,04-24-feat_fr-2701_...), the hostname becomesfr-XXXX.localhost:1355. - Otherwise it falls back to
portless run, which yields<branch>.<project>.localhost:1355automatically.
Why issue-number names: long branch names trigger a TLS-cert generation issue under HTTPS. Short, predictable names sidestep that and are also easier to read and bookmark.
By default Portless assigns a random free port to CRA (4000–4999). If you need a fixed port — for example to point an existing browser tab or external integration at it — set PORT when running dev:
PORT=9081 pnpm run devPortless then proxies https://<name>.localhost:1355 to http://localhost:9081, and CRA listens directly on 9081.
Each worktree picks up its own branch's FR number, so two worktrees on different issues (fr-2701 and fr-2890) coexist on fr-2701.localhost:1355 and fr-2890.localhost:1355 without conflict. Two worktrees on the same branch will collide; dev.mjs passes --force so the second one overrides the first registration. Run only one of them at a time.
Create .env.development.local (copy from .env.development.local.sample) and set:
VITE_THEME_HEADER_COLOR=#7C3AEDVite auto-loads VITE_* vars from this file and exposes them on import.meta.env for the React app, tinting the header so you can tell multiple instances apart at a glance. You can also export VITE_THEME_HEADER_COLOR in the shell — same effect, no file edit needed.
pnpm --filter backend.ai-ui run storybookRuns behind Portless on a fixed internal port 6006. Open the printed *.localhost:1355 URL.
- Browser cannot reach
*.localhost— runsudo pnpm exec portless hosts sync. Safari requires this; Chrome and Firefox usually work without it. - Service-worker error like
Script .../sw.js load failed— fixed in this branch (index.htmlnow skips SW registration on*.localhost). If you still see it, unregister the stale SW via DevTools → Application → Service Workers. already registered by a running process— Portless route from a previously killed dev server still exists.dev.mjspasses--forceso this should self-heal; if not, runpnpm exec portless proxy stop && pnpm exec portless proxy start -p 1355once.- HTTPS request hangs /
HTTP 000— TLS cert generation can fail for very long hostnames. Either rename the branch to includeFR-XXXX, or switch the daemon to HTTP withpnpm exec portless proxy start -p 1355 --no-tls. - Theme color not applied — confirm
VITE_THEME_HEADER_COLORis set in.env.development.localor the shell, and restartpnpm run dev. Vite reads env at server start, so existing dev servers won't pick up changes until restarted. - Watchers seem stuck — all three dev children (tsc, Relay watch, CRA) run under
concurrentlywith--kill-others; Ctrl+C tears them down together.
| Command | Description |
|---|---|
pnpm run dev |
TypeScript watch + Relay watch + CRA dev server, all under Portless |
PORT=9081 pnpm run dev |
Same, but pin CRA to port 9081 |
pnpm run wsproxy |
WebSocket proxy on fixed port 5050 (not wrapped by Portless) |
pnpm --filter backend.ai-ui run storybook |
Storybook under Portless |
pnpm exec portless list |
Show active Portless routes |
pnpm exec portless proxy stop / start -p 1355 [--no-tls] |
Daemon control (project-local binary) |