.agents/skills/portless/SKILL.md
name: portless description: Start and debug Tranzfer local URLs through Portless. Use when running vp run dev, opening tranzfer.localhost, fixing EADDRINUSE, random Vite ports, CORS or OAuth redirects, or when a tab is on localhost:3000–3003.
Portless in this repo
Named hosts are the local app. Ports behind the proxy are ephemeral. Installed
docs: node_modules/portless/README.md. Pin is portless@0.15.6.
Start
From the repo root:
| 1 | vp run dev |
That is portless running the single tranzfer app in portless.json, which
runs alchemy dev from infra/alchemy.run.ts (stage dev_$USER, Workers in
local workerd). D1 and R2 are real Cloudflare resources for that stage
(Alchemy.remote() in apps/api/src/resources.ts), shared by every checkout
of the same user. The bucket's CORS allows only APP_URL, so the last checkout
to start wins browser uploads.
The proxy serves TLS. Hosts:
| 1 | https://tranzfer.localhost web Worker on :3000 |
| 2 | https://api.tranzfer.localhost api Worker on :8787 |
The api host is a static alias, registered once with portless alias api.tranzfer 8787. turbo is false. Do not turn it on.
From an agent shell, start detached so tool timeouts cannot kill it:
| 1 | pkill -f alchemy/bin/exec; pkill -f "cli.js dev" |
| 2 | setsid -f sh -c 'cd "$(git rev-parse --show-toplevel)" && exec vp run dev > /tmp/dev.log 2>&1 < /dev/null' |
Worker logs live under infra/.alchemy/log/<stage>/{Api,Web}.
Do not
vp devorvp run --filter @tranzfer/web devas the everyday server. Those bind Vite'sserver.port(3000) or the next free 3001–3003. Agents then open the wrong tab.- Hard-code
localhost:3000in docs, CORS, OAuth, or.env. npx/dlxPortless. Use the workspace package.portless trustfor this repo. The proxy already serves TLS; usecurl -k.--lan,--tailscale,--ngrok, or Funnel unless the user asked to share.
How the proxy works here
Portless picks a free app port (typically 4000–4999), injects PORT and Vite
or Wrangler --port, and serves the named .localhost host on loopback HTTP
(port 80 with --no-tls). The browser talks to the name, not the ephemeral
port. HMR websocket goes through the proxy.
apps/web/vite.config.ts server.port: 3000 is only the unproxied fallback.
Debug
| 1 | portless list |
| 2 | portless doctor |
| 3 | portless prune |
list shows name → ephemeral port. doctor checks proxy, DNS, routes.
prune kills orphaned servers from crashed sessions. After a messy agent
session, prune before starting another vp run dev.
If the page loops to itself (508), an API proxy must changeOrigin and target
http://api.tranzfer.localhost, not localhost:<web-port>.
Worktrees get a branch subdomain automatically (http://<branch>.tranzfer.localhost).
Deploy and Alchemy still run from the main checkout.