---
title: "Portless"
description: "Named HTTPS .localhost URLs with portless (v0.15.x). Eliminates port collisions, gives agents stable URLs, adds branch-named subdomains for git worktrees, LAN mode (--lan), and Tailscale sharing. Use when setting up a local dev environment or testing from phones and tablets on the same wifi. Do NOT use for production deployments, CI environments (set PORTLESS=0), or DNS/hosting configuration."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/portless"
---

# Portless

Named HTTPS .localhost URLs with portless (v0.15.x). Eliminates port collisions, gives agents stable URLs, adds branch-named subdomains for git worktrees, LAN mode (--lan), and Tailscale sharing. Use when setting up a local dev environment or testing from phones and tablets on the same wifi. Do NOT use for production deployments, CI environments (set PORTLESS=0), or DNS/hosting configuration.

<span className="badge badge-gray">Reference</span> <span className="badge badge-green">low</span>

> **Auto-activated** — this skill loads automatically when Claude detects matching context.

<ContextualSkillSidebar slug="portless" />

> **Portless** Named HTTPS .localhost URLs with portless (v0.15.x). Eliminates port collisions, gives agents stable URLs, adds branch-named subdomains for git worktrees, LAN mode (--lan), and Tailscale sharing. Use when setting up a local dev environment or testing from phones and tablets on the same wifi. Do NOT use for production deployments, CI environments (set PORTLESS=0), or DNS/hosting configuration.


# Portless Integration

Named `.localhost` URLs for local development. Replaces `localhost:3000` with `https://myapp.localhost`.

> **Full CLI reference**: Load `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/portless/references/upstream.md")` for complete command docs.

## New in 2026-04 → 2026-07 (portless 0.10.x → 0.15.0)

- **`portless doctor` (0.15.0)** — read-only diagnostics that check Node.js, the state directory, proxy liveness, route entries, hostname resolution, HTTPS CA trust, and LAN prerequisites, then print suggested fixes. Run it before filing an issue or when a `.localhost` URL won't resolve.
- **HTTP/2 Host forwarding fix (0.15.0)** — the proxy now forwards the HTTP/2 `:authority` as `Host` to HTTP/1.1 backends, fixing apps that read `Host` and previously saw `127.0.0.1` for browser traffic. `--force` takeover cleanup now removes only routes still owned by the exiting process, so a forced takeover no longer deregisters the new owner's route.
- **`--ngrok` flag (0.14.0)** — share an app publicly via ngrok while local access keeps its `.localhost` URL. Pair with the existing Tailscale/Funnel options when you need a public URL without giving up the named-subdomain dev experience.
- **Node.js 24+ required (0.13.1, BREAKING)** — the proxy and CLI now require Node.js 24 or newer; older runtimes are unsupported. This release also hardens startup-service persistence so `.localhost` URLs survive reboot reliably.
- **State directory moved to `~/.portless` (0.11, BREAKING)** — state relocated from scattered/temp locations to `~/.portless`; override with `PORTLESS_STATE_DIR`. Old state from pre-0.11 installs is not migrated automatically.
- **OS startup service (0.13.0)** — `portless service install` / `service status` / `service uninstall` register a native startup service for the HTTPS proxy across macOS launchd, Linux systemd, and Windows Task Scheduler. `.localhost` URLs survive reboot without a manual `portless proxy start`. `portless clean` removes the service alongside CA + hosts cleanup.
- **Tailscale readiness preflight (0.13.0)** — `--tailscale` and `--funnel` now validate Tailscale HTTPS + Funnel prerequisites before starting the child process, surfacing actionable errors instead of hanging during registration.
- **Tailscale integration (0.12.0)** — `--tailscale` shares your app over your tailnet with automatic HTTPS on port 443; `--funnel` exposes it publicly via Tailscale Funnel. Apps receive `PORTLESS_TAILSCALE_URL` so they can reference their own public address. `portless list` now shows tailnet URLs.
- **Zero-config mode (0.11.0)** — bare `portless` auto-discovers dev scripts from `package.json`. Multi-app monorepos get automatic subdomain assignment; Turborepo task-graph integration is wired in. `portless.json` config file supported. `--script` overrides the default "dev" script.
- **`portless prune`** — removes orphaned dev servers and stale Tailscale registrations.
- **`portless clean` (extended)** — now also tears down Tailscale registrations alongside CA + hosts cleanup.
- **Rsbuild + VitePlus auto-port injection** — same auto-wiring as Vite/Next.
- **State directory** moved to `~/.portless` (was scattered).
- **HTTPS on 443 by default** (breaking from 0.9.x http:1355). Valid cert, no setup. `--no-tls` reverts.
- **`NODE_EXTRA_CA_CERTS` auto-injected (0.10.2)** into child processes — node HTTPS calls trust portless CA with zero setup.
- **`--wildcard` subdomains** — `https://*.myapp.localhost` for multi-tenant / preview routing.
- **`portless alias &lt;name&gt; &lt;port&gt;`** — map a docker-compose / emulate port to a named URL without a long-running `run` process.
- **`portless clean`** — full teardown: stops proxy, removes CA, wipes state, cleans `/etc/hosts`.
- **`--lan` mode** — mDNS `.local` hostnames reachable across wifi (phone, tablet, other machines) without router config.
- **Fixed app ports** — `--app-port 3000` / `PORTLESS_APP_PORT` for tools that need a known port (debuggers, docker).
- **hosts-sync on by default** for Safari compat (disable with `PORTLESS_SYNC_HOSTS=0`).
- **HTTP/2 HMR fixes** for Vite/VitePlus/Next.js dev — websocket upgrades no longer break under h2.
- **Expo / React Native** support — `portless run expo start` gives Metro a stable URL for device QR codes.

## When to Use

- Starting a dev server that agents or browser tests will target
- Running multiple services locally (API + frontend + docs)
- Working in git worktrees (branch-named subdomains)
- Local OAuth flows (stable callback URLs)
- Connecting emulate API mocks to named URLs

## Quick Start

```bash
# Instead of: npm run dev (random port)
portless run npm run dev
# → https://myapp.localhost (stable, named, HTTPS on 443 — default in 0.10+)

# Multi-service
portless run --name api npm run dev:api
portless run --name web npm run dev:web
# → https://api.localhost, https://web.localhost

# LAN mode (0.10.0) — reachable from phone/tablet via mDNS
portless proxy start --lan
portless run npm run dev
# → https://myapp.local (resolves across the local network, no router config)

# Full teardown (0.10.1) — stops proxy, removes CA, wipes state, cleans /etc/hosts
portless clean

# Boot persistence (0.13.0) — install native startup service (launchd / systemd / Task Scheduler)
portless service install
portless service status
# Removed automatically by `portless clean`, or explicitly:
portless service uninstall
```

> **0.10.x breaking change:** default switched from `https://app.localhost` to `https://app.localhost` on port 443. Use `--no-tls` to revert. `NODE_EXTRA_CA_CERTS` is injected into child processes automatically (0.10.2) — no manual cert setup. `/etc/hosts` is synced automatically for Safari; disable with `PORTLESS_SYNC_HOSTS=0`.

## Framework-Specific Setup

> Load `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/portless/references/framework-integration.md")` for full framework recipes.

Most frameworks (Next.js, Vite, Express) work with `portless run &lt;cmd&gt;`. Some need explicit flags:

| Framework | Auto-detected? | Extra flags needed |
|-----------|:-:|---|
| Next.js | Yes | None |
| Vite / Astro | Yes | None |
| Express / Fastify / Hono | Yes | None (reads `PORT` env var) |
| Ruby on Rails | Yes | None |
| FastAPI / uvicorn | **No** | `--port $PORT --host $HOST` |
| Django | **No** | `$HOST:$PORT` positional arg |

## Why `.localhost`?

| Feature | `.localhost` (RFC 6761) | `127.0.0.1:PORT` | `/etc/hosts` hack |
|---------|:-:|:-:|:-:|
| No `/etc/hosts` editing | Yes | Yes | No |
| HTTPS with valid cert | Yes | No | Manual |
| Wildcard subdomains | Yes | No | No |
| Works in all browsers | Yes | Yes | Varies |
| Cookie isolation per service | Yes | No | Yes |
| No port conflicts | Yes | No | Yes |

## Key Environment Variables

When portless runs your command, it injects:

| Variable | Value | Use in agents |
|----------|-------|---------------|
| `PORT` | Assigned ephemeral port (4000-4999) | Internal only |
| `HOST` | `127.0.0.1` | Internal only |
| `PORTLESS_URL` | `https://myapp.localhost` | **Use this in agent prompts** |
| `NODE_EXTRA_CA_CERTS` | Path to portless CA *(auto-injected 0.10.2)* | Child node processes trust portless certs without setup |

### Toggle env vars

| Variable | Effect |
|----------|--------|
| `PORTLESS=0` | Bypass portless entirely (CI) |
| `PORTLESS_SYNC_HOSTS=0` | Disable auto `/etc/hosts` sync (default: on in 0.10.1+) |
| `PORTLESS_STATE_DIR` | Override state dir (default: `~/.portless` or `/tmp/portless` for privileged ports) |

## OrchestKit Integration Patterns

### 1. Agent-Accessible Dev Server

```bash
# Start with portless, then agents can target PORTLESS_URL
portless run npm run dev

# In ork:expect or agent-browser:
agent-browser open $PORTLESS_URL
```

### 2. Emulate + Portless (Named API Mocks)

```bash
# Register emulate ports as named aliases
portless alias github-api 4001
portless alias vercel-api 4000
portless alias google-api 4002

# Now agents can target:
#   https://github-api.localhost — GitHub emulator
#   https://vercel-api.localhost — Vercel emulator
```

### 3. Git Worktree Dev

```bash
# In worktree for feature/auth-flow:
portless run npm run dev
# → https://auth-flow.myapp.localhost (auto branch prefix)
```

### 4. Bypass in CI

```bash
# Disable portless in CI — direct port access
PORTLESS=0 npm run dev
```

## Anti-Patterns

| Don't | Do Instead |
|-------|------------|
| Hardcode `localhost:3000` in tests | Use `PORTLESS_URL` or `process.env.PORTLESS_URL` |
| Run portless in CI | Set `PORTLESS=0` in CI environments |
| Use numeric ports in AGENTS.md | Document the portless URL |

## References

| File | Content |
|------|---------|
| `references/upstream.md` | Full portless CLI reference (synced from Vercel) |
| `references/upstream-oauth.md` | OAuth callback patterns with stable URLs |
| `references/framework-integration.md` | Framework recipes (FastAPI, Django, Docker, gotchas) |
| `checklists/new-project-setup.md` | Step-by-step: add portless to a new project |


---

## References (3)

### Framework Integration

# Framework Integration Recipes

How to configure popular frameworks to work with `portless run`.

## How Portless Works

Portless injects `PORT` and `HOST` environment variables into your process. Frameworks that read `PORT` automatically (Express, Fastify, Hono, Next.js, Vite) work out of the box. Others need explicit flags.

## Framework Reference

### Node.js (Express, Fastify, Hono)

Auto-detected. No extra flags needed:

```bash
portless run --name api node server.js
# Express/Fastify/Hono read process.env.PORT automatically
```

**IPv6 ECONNREFUSED fix**: If you see `ECONNREFUSED ::1:4001`, Node 18+ prefers IPv6 by default. Fix:

```js
// At the top of your entry file
import dns from "node:dns";
dns.setDefaultResultOrder("ipv4first");
```

Or set the env var: `NODE_OPTIONS="--dns-result-order=ipv4first"`.

### Next.js

Auto-detected. Reads `PORT` from environment:

```bash
portless run --name web next dev
# → https://web.localhost
```

**Proxy rewrites** (e.g., `/api` → backend service): use `changeOrigin: true` to avoid 508 redirect loops:

```js
// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: "/api/:path*",
        destination: "https://api.localhost/:path*",
        // CRITICAL: prevents 508 loop when proxying to another portless service
        changeOrigin: true,
      },
    ];
  },
};
```

### Vite / Astro

Auto-detected. Reads `PORT` from environment:

```bash
portless run --name app vite dev
# → https://app.localhost
```

**Proxy config** for API backends:

```js
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      "/api": {
        target: "https://api.localhost",
        changeOrigin: true, // prevents 508 loop
        secure: false, // allow self-signed .localhost certs
      },
    },
  },
});
```

### FastAPI / Uvicorn

**NOT auto-detected** — uvicorn ignores the `PORT` env var. You MUST pass `--port` and `--host` explicitly:

```bash
# WRONG — uvicorn ignores PORT, binds to 8000
portless run --name api uvicorn main:app

# CORRECT — read from injected env vars
portless run --name api uvicorn main:app --port \$PORT --host \$HOST
```

**Trailing slash gotcha**: FastAPI redirects `/api/users` to `/api/users/` by default. This 307 redirect can confuse proxied setups. Fix:

```python
from fastapi import FastAPI

app = FastAPI(redirect_slashes=False)
```

Or ensure all routes have consistent trailing slashes.

### Django

**NOT auto-detected** — pass `$HOST:$PORT` as a positional argument:

```bash
# WRONG — Django ignores PORT env var
portless run --name admin python manage.py runserver

# CORRECT
portless run --name admin python manage.py runserver \$HOST:\$PORT
```

### Ruby on Rails

Auto-detected. Reads `PORT` from environment:

```bash
portless run --name app rails server
```

### Go (net/http)

Read from environment in your code:

```go
port := os.Getenv("PORT")
if port == "" {
    port = "8080"
}
http.ListenAndServe(":"+port, handler)
```

```bash
portless run --name api go run ./cmd/server
```

## Docker Infrastructure via Aliases

Use `portless alias` for Docker services that expose fixed ports (databases, caches, etc.):

```bash
# Map Docker-exposed ports to named URLs
portless alias postgres 5432
portless alias redis 6379
portless alias minio 9000

# Now accessible as:
#   https://postgres.localhost
#   https://redis.localhost
#   https://minio.localhost
```

This is useful for consistency — all services (app and infra) use the same `*.localhost` URL pattern.

## Common Gotchas

### Cookie Domain

Never set `Domain=.localhost` on cookies. Per RFC 6761, browsers handle `.localhost` specially and will reject domain-scoped cookies. Use path-scoped cookies or omit the `Domain` attribute entirely (defaults to exact origin).

```js
// WRONG
res.cookie("session", token, { domain: ".localhost" });

// CORRECT — omit domain, browser scopes to exact origin
res.cookie("session", token, { path: "/" });
```

### Proxy 508 Loop

When proxying between two portless services (e.g., `web.localhost` → `api.localhost`), always set `changeOrigin: true` in your proxy config. Without it, the `Host` header retains the original domain, causing a redirect loop (HTTP 508).

### Mixed Port References

After adopting portless, grep your codebase for hardcoded `localhost:NNNN` references:

```bash
grep -rn 'localhost:[0-9]' --include='*.ts' --include='*.js' --include='*.env*' .
```

Replace with `PORTLESS_URL` or the named `*.localhost` URL.


### Upstream Oauth

&lt;!-- SYNCED from vercel-labs/portless (skills/oauth/SKILL.md) --&gt;
&lt;!-- Hash: ea1ec2f9e933d0d258bf3493242244ea562c3428ba7f269e267650e96d28cf60 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# OAuth with Portless

OAuth providers validate redirect URIs against domain rules. `.localhost` subdomains fail on most providers because they are not in the Public Suffix List or are explicitly blocked. Portless fixes this with `--tld` to serve apps on real, valid domains.

## The Problem

When portless uses the default `.localhost` TLD, OAuth providers reject redirect URIs like `http://myapp.localhost:1355/callback`:

| Provider  | `localhost` | `.localhost` subdomains | Reason                         |
| --------- | ----------- | ----------------------- | ------------------------------ |
| Google    | Allowed     | Rejected                | Not in their bundled PSL       |
| Apple     | Rejected    | Rejected                | No localhost at all            |
| Microsoft | Allowed     | Allowed                 | Permissive localhost handling  |
| Facebook  | Allowed     | Varies                  | Must register each URI exactly |
| GitHub    | Allowed     | Allowed                 | Permissive                     |

Google and Apple are the strictest. Microsoft and GitHub are more lenient with localhost.

## The Fix

Use a valid TLD so the redirect URI passes provider validation:

```bash
portless proxy start --tld dev
portless myapp next dev
# -> https://myapp.dev
```

Any TLD in the Public Suffix List works: `.dev`, `.app`, `.com`, `.io`, etc.

### Use a domain you own

Bare TLDs like `.dev` mean `myapp.dev` could collide with a real domain. Use a multi-segment TLD under a domain you control, so the app name stays clean and the domain structure lives in the TLD:

```bash
portless proxy start --tld local.yourcompany.dev
portless myapp next dev
# -> https://myapp.local.yourcompany.dev
```

This ensures no outbound traffic reaches something you don't own. For teams, set a wildcard DNS record (`*.local.yourcompany.dev -> 127.0.0.1`) so every developer gets resolution without `/etc/hosts`, and every developer shares the same redirect URIs in the provider console.

## Provider Setup

### Google

1. Go to [Google Cloud Console > Credentials](https://console.cloud.google.com/apis/credentials)
2. Create or edit an OAuth 2.0 Client ID (Web application)
3. Add the portless domain to **Authorized JavaScript origins**: `https://myapp.dev`
4. Add the callback to **Authorized redirect URIs**: `https://myapp.dev/api/auth/callback/google`

Google validates domains against the Public Suffix List. The domain must end with a recognized TLD. `.localhost` subdomains fail this check; `.dev`, `.app`, `.com`, etc. all pass.

HTTPS is required for `.dev` and `.app` (HSTS-preloaded). Portless handles this automatically with `--https`.

### Apple

Apple Sign In does not allow `localhost` or IP addresses at all.

1. Go to [Apple Developer > Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources)
2. Register a Services ID
3. Configure Sign In with Apple, adding the portless domain as a **Return URL**: `https://myapp.dev/api/auth/callback/apple`

The domain must be a real, publicly-resolvable domain name. Since portless maps the domain to 127.0.0.1 locally, the browser resolves it but Apple's server-side validation may require the domain to resolve publicly too. If Apple rejects the domain, add a public DNS A record pointing to 127.0.0.1 for your dev subdomain.

### Microsoft (Entra / Azure AD)

1. Go to [Azure Portal > App registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)
2. Create or edit an app registration
3. Under **Authentication**, add a **Web** redirect URI: `https://myapp.dev/api/auth/callback/azure-ad`

Microsoft allows `http://localhost` with any port for development. It also accepts `.localhost` subdomains in most cases. Using a custom TLD with portless is still recommended for consistency across providers.

### Facebook (Meta)

1. Go to [Meta for Developers > App Dashboard](https://developers.facebook.com/apps/)
2. Under **Facebook Login > Settings**, add the portless URL to **Valid OAuth Redirect URIs**: `https://myapp.dev/api/auth/callback/facebook`

Facebook requires each redirect URI to be registered exactly (no wildcards). Strict Mode (enabled by default) enforces exact matching.

### GitHub

1. Go to [GitHub Developer Settings > OAuth Apps](https://github.com/settings/developers)
2. Set **Authorization callback URL**: `https://myapp.dev/api/auth/callback/github`

GitHub is permissive with localhost and subdomains. A custom TLD is not strictly required but keeps the setup consistent.

## Auth Library Configuration

### NextAuth / Auth.js

Set `NEXTAUTH_URL` to match the portless domain:

```text
NEXTAUTH_URL=https://myapp.dev
```

NextAuth uses this to construct callback URLs. Without it, callbacks may use `localhost` and cause a mismatch.

### Passport.js

Set the `callbackURL` in each strategy to use the portless domain:

```js
new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: process.env.BASE_URL + "/auth/google/callback",
});
```

Set `BASE_URL=https://myapp.dev` in your environment.

### Generic / Manual

Read the `PORTLESS_URL` environment variable that portless injects into the child process:

```js
const baseUrl = process.env.PORTLESS_URL || "http://localhost:3000";
const callbackUrl = `${baseUrl}/auth/callback`;
```

## Troubleshooting

### "redirect_uri_mismatch" or "invalid redirect URI"

The redirect URI sent during the OAuth flow doesn't match what's registered with the provider. Check:

1. The provider's registered redirect URI matches the portless domain exactly (protocol, host, path)
2. `NEXTAUTH_URL` or equivalent is set to the portless URL (not `localhost`)
3. The proxy is running with the correct TLD (`portless list` to verify)

### Provider requires HTTPS

`.dev` and `.app` TLDs are HSTS-preloaded, so browsers force HTTPS. Start the proxy:

```bash
portless proxy start --tld dev
```

Portless defaults to HTTPS on port 443 (auto-elevates with sudo). Run `portless trust` to add the local CA to your system trust store and eliminate browser warnings.

### Apple rejects the domain

Apple may require the domain to resolve publicly. Add a DNS A record for your dev subdomain pointing to `127.0.0.1`:

```
myapp.local.yourcompany.dev  A  127.0.0.1
```

Or use a wildcard: `*.local.yourcompany.dev  A  127.0.0.1`.

### Callback goes to wrong URL after sign-in

The auth library is constructing the callback URL from `localhost` instead of the portless domain. Set the appropriate environment variable:

- **NextAuth**: `NEXTAUTH_URL=https://myapp.dev`
- **Auth.js v5**: `AUTH_URL=https://myapp.dev`
- **Manual**: `PORTLESS_URL` is injected automatically; use it as the base URL

## Example

See [`examples/google-oauth`](../../examples/google-oauth) for a complete working example with Next.js + NextAuth + Google OAuth using `--tld dev`.


### Upstream

&lt;!-- SYNCED from vercel-labs/portless (skills/portless/SKILL.md) --&gt;
&lt;!-- Hash: e2b0cf9ec19baa96004442d444552528d19ad45a368153efa837ea516867d443 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# Portless

Replace port numbers with stable, named .localhost URLs. For humans and agents.

## Why portless

- **Port conflicts**: `EADDRINUSE` when two projects default to the same port
- **Memorizing ports**: which app is on 3001 vs 8080?
- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content
- **Monorepo multiplier**: every problem scales with each service in the repo
- **Agents test the wrong port**: AI agents guess or hardcode the wrong port
- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift
- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change
- **Sharing URLs with teammates**: "what port is that on?" becomes a Slack question
- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects

## Installation

Install globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.

```bash
# Global (available everywhere)
npm install -g portless

# Or per-project dev dependency
npm install -D portless
```

When installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).

## Quick Start

```bash
# Install globally (or add -D to a project)
npm install -g portless

# Run your app (auto-starts the HTTPS proxy on port 443)
portless run next dev
# -> https://<project>.localhost

# Or with an explicit name
portless myapp next dev
# -> https://myapp.localhost
```

The proxy auto-starts when you run an app. You can also start it explicitly with `portless proxy start`. Auto-start reuses the configuration (port, TLS, TLDs) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars always take priority.

In non-interactive environments (no TTY, or `CI=1`), portless exits with a descriptive error instead of prompting. Task runners like turborepo should pre-start the proxy.

## Integration Patterns

### Zero-config (recommended)

Bare `portless` works out of the box. It runs the `"dev"` script from `package.json` through the proxy, inferring the app name from the package name, git root, or directory:

```bash
portless        # -> runs "dev" script, https://<project>.localhost
pnpm dev        # -> works without portless, plain "next dev"
```

Use an optional `portless.json` to override defaults (name, script, port):

```json
{ "name": "myapp" }
```

```bash
portless        # -> runs "dev" script, https://myapp.localhost
```

### Monorepo

One `portless.json` at the repo root. Portless discovers packages from `pnpm-workspace.yaml`, or the `"workspaces"` field in `package.json` (npm, yarn, bun):

```json
{
  "apps": {
    "apps/web": { "name": "myapp" },
    "apps/api": { "name": "api.myapp" }
  }
}
```

```bash
portless                  # from repo root: start all packages with a "dev" script
cd apps/web && portless   # start just one package
portless --script start   # run "start" instead of "dev"
```

The `apps` map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.

Without an `apps` map, hostnames follow `&lt;package&gt;.&lt;project&gt;.localhost`. The project name comes from the most common npm scope (e.g. `@myorg/web` and `@myorg/api` produce `myorg`), falling back to the workspace root directory name. If a package's short name matches the project name, it uses the bare `&lt;project&gt;.localhost`.

### Turborepo

For turborepo projects, use portless as the `dev` script with the real command in a separate script:

```json
{
  "scripts": { "dev": "portless", "dev:app": "next dev" },
  "portless": { "name": "myapp", "script": "dev:app" }
}
```

`pnpm dev` runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy.

### package.json scripts

You can still use portless directly in scripts:

```json
{
  "scripts": {
    "dev": "portless run next dev"
  }
}
```

The proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.

### Multi-app setups with subdomains

```bash
portless myapp next dev          # https://myapp.localhost
portless api.myapp pnpm start    # https://api.myapp.localhost
portless docs.myapp next dev     # https://docs.myapp.localhost
```

By default, only explicitly registered subdomains are routed (strict mode). Start the proxy with `--wildcard` to allow any subdomain of a registered route to fall back to that app (e.g. `tenant1.myapp.localhost` routes to the `myapp` app). Exact matches always take priority over wildcards.

### Git worktrees

`portless run` automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain prefix so each worktree gets a unique URL:

```bash
# Main worktree (no prefix)
portless run next dev   # -> https://myapp.localhost

# Linked worktree on branch "fix-ui"
portless run next dev   # -> https://fix-ui.myapp.localhost
```

No config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.

### Bypassing portless

Set `PORTLESS=0` to run the command directly without the proxy:

```bash
PORTLESS=0 pnpm dev   # Bypasses proxy, uses default port
```

## How It Works

1. `portless proxy start` starts an HTTPS reverse proxy on port 443 as a background daemon. Auto-elevates with sudo on macOS/Linux; falls back to port 1355 if sudo is unavailable. Use `--no-tls` for plain HTTP on port 80. Configurable with `-p` / `--port` or the `PORTLESS_PORT` env var. The proxy also auto-starts when you run an app.
2. `portless &lt;name&gt; &lt;cmd&gt;` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy
3. The browser hits `https://&lt;name&gt;.localhost`; the proxy forwards to the app's assigned port

Outside LAN mode, the proxy and its HTTP redirect listener bind only to the IPv4 and IPv6 loopback addresses, `127.0.0.1` and `::1`. They do not accept connections through LAN, VPN, or other network interfaces.

`.localhost` domains resolve to `127.0.0.1` natively in Chrome, Firefox, and Edge. Safari relies on the system DNS resolver, which may not handle `.localhost` subdomains on all configurations. Run `portless hosts sync` to add entries to `/etc/hosts` if needed.

Use `portless proxy start --tld localhost --tld test` to serve the same app names under multiple TLDs from one proxy. `PORTLESS_URL` uses the first configured TLD. When configured TLDs overlap (e.g. `example.com` and `dev.example.com`), hostnames are matched against the longest TLD first, regardless of configuration order. `PORTLESS_TLD` accepts the same comma separated list format, e.g. `PORTLESS_TLD=localhost,test`.

TLDs can be multi-segment DNS names such as `dev.example.com`, so local URLs can mirror production structure (`myapp.dev.example.com`). Each label follows DNS rules: lowercase letters, digits, interior hyphens, 63 characters per label, 253 total. Strict OAuth providers that reject `.localhost` redirect URIs accept a real domain like `https://myapp.dev.example.com/api/auth/callback/google`.

Most frameworks (Next.js, Express, Nuxt, etc.) respect the `PORT` env var automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the correct `--port` flag and, when needed, a matching `--host` CLI flag. Injection reaches through a package script whose command starts with the framework or a known runner (`"dev": "vite"`, `"dev": "bunx vite"`). Only the framework's server commands get the flags (`dev`, `serve`, `preview`, `start`, a bare `vite`, or `vite [root]`); a command that does not serve, such as `vite build`, `vite optimize`, `vp test` or `astro check`, rejects them and is left alone. Expo connection modes (`--localhost`, `--lan`, `--tunnel`) are preserved while the assigned port is still injected. A script portless cannot classify is left alone too: a flag before the subcommand on a CLI whose flag grammar it does not track (`vp --mode dev build`). Portless also leaves a script alone when appending flags to it would not work: a compound command (`&&`, `|`, `;`), a trailing `#` comment, its own `--` option terminator, an env prefix (`NODE_ENV=production vite`), delegation to another script (`"dev": "npm run dev:vite"`), or runner flags before the script name (`bun run --bun dev`). Those keep their own port, so set it in the script yourself.

### State directory

Portless stores its state (routes, PID file, port file) in `~/.portless`. When the proxy runs under sudo, this remains the invoking user's home directory so unprivileged apps and the proxy share route registrations. Override with the `PORTLESS_STATE_DIR` environment variable.

### Environment variables

| Variable              | Description                                                                    |
| --------------------- | ------------------------------------------------------------------------------ |
| `PORTLESS_PORT`       | Override the default proxy port (default: 443 with HTTPS, 80 without)          |
| `PORTLESS_APP_PORT`   | Use a fixed port for the app (skip auto-assignment)                            |
| `PORTLESS_HTTPS`      | HTTPS on by default; set to `0` to disable (same as `--no-tls`)                |
| `PORTLESS_LAN`        | Set to `1` to always enable LAN mode (auto-detects LAN IP)                     |
| `PORTLESS_LAN_IP`     | Pin a specific LAN IP for LAN mode                                             |
| `PORTLESS_TLD`        | Use one or more TLDs, single or multi-segment (e.g. localhost,dev.example.com) |
| `PORTLESS_WILDCARD`   | Set to `1` to allow unregistered subdomains to fall back to parent             |
| `PORTLESS_SYNC_HOSTS` | Set to `0` to disable auto-sync of /etc/hosts (on by default)                  |
| `PORTLESS_TAILSCALE`  | Set to `1` to share apps on your Tailscale network (same as `--tailscale`)     |
| `PORTLESS_FUNNEL`     | Set to `1` to share apps publicly via Tailscale Funnel (same as `--funnel`)    |
| `PORTLESS_NGROK`      | Set to `1` to share apps publicly via ngrok (same as `--ngrok`)                |
| `PORTLESS_STATE_DIR`  | Override the state directory                                                   |
| `PORTLESS=0`          | Bypass the proxy, run the command directly                                     |

### HTTP/2 + HTTPS

HTTPS with HTTP/2 is enabled by default (faster page loads for dev servers with many files). WebSockets work over both HTTP/1.1 (Upgrade) and HTTP/2 (RFC 8441 extended CONNECT), so dev server HMR works through the proxy. First run generates a local CA and adds it to the system trust store. After that, no prompts and no browser warnings.

```bash
portless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs
portless proxy start --no-tls                       # Disable HTTPS (plain HTTP)
portless trust                                      # Add CA to trust store later
```

On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store. On WSL, it updates both the Linux trust store and the Windows current-user Root store so Windows browsers trust portless HTTPS certificates.

### LAN mode

```bash
portless proxy start --lan
portless proxy start --lan --https
portless proxy start --lan --ip 192.168.1.42
```

`--lan` explicitly binds the proxy to the IPv4 and IPv6 unspecified addresses, `0.0.0.0` and `::`, and advertises `&lt;name&gt;.local` hostnames over mDNS so devices on the same Wi-Fi can reach your apps. Portless auto-detects your LAN IP and follows network changes automatically, but you can pin a specific address with `--ip &lt;address&gt;` or the `PORTLESS_LAN_IP` environment variable. Set `PORTLESS_LAN=1` to default to LAN mode every time the proxy starts.

Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start again, it stays in LAN mode. All proxy settings (port, TLS, TLDs, LAN) are persisted and reused on auto-start unless overridden by explicit flags or env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.

LAN mode depends on the system mDNS helpers that portless launches: macOS includes `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s tooling).

- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:

  ```js
  // next.config.js
  module.exports = {
    allowedDevOrigins: ["myapp.local", "*.myapp.local"],
  };
  ```

- **Expo / React Native**: portless always injects `--port`. React Native also gets `--host 127.0.0.1`. Expo gets `--host localhost` outside LAN mode, but in LAN mode portless leaves Metro on its default LAN host behavior instead of forcing `--host` or `HOST`.

### Tailscale sharing

Share dev servers with teammates on your Tailscale network using `--tailscale`, or expose to the public internet with `--funnel`:

```bash
portless myapp --tailscale next dev
# -> https://myapp.localhost           (local)
# -> https://devbox.yourteam.ts.net    (tailnet)

portless myapp --funnel next dev
# -> https://myapp.localhost           (local)
# -> https://devbox.yourteam.ts.net    (public internet)
```

Tailscale HTTPS certificates must be enabled before `--tailscale` or `--funnel` can register HTTPS URLs. Funnel must also be enabled for the tailnet and node before `--funnel` can register the public URL. If either setting is missing, portless exits before starting the child process.

Each `--tailscale` app is root-mounted on its own Tailscale HTTPS port (443, then 8443, 8444, etc.) so no framework `basePath` configuration is needed. Set `PORTLESS_TAILSCALE=1` to share every app by default. `portless list` shows both local and tailnet URLs. Tailscale serve registrations are cleaned up when the app exits. Requires `tailscale` CLI installed and connected, with Tailscale HTTPS certificates enabled.

### ngrok sharing

Expose a dev server to the public internet with ngrok using `--ngrok`:

```bash
portless myapp --ngrok next dev
# -> https://myapp.localhost           (local)
# -> https://abc123.ngrok.app          (public internet)
```

Set `PORTLESS_NGROK=1` to enable ngrok by default when portless runs an app. `portless list` shows both local and ngrok URLs. The ngrok tunnel is cleaned up when the app exits. Requires the `ngrok` CLI to be installed and authenticated with `ngrok config add-authtoken &lt;token&gt;`.

## OS startup service

Use the service command when users want the proxy to start automatically after reboot:

```bash
portless service install
portless service install --lan
portless service install --wildcard
PORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install
portless service status
portless service uninstall
```

The service uses portless defaults unless install options or `PORTLESS_*` environment variables are provided: HTTPS on port 443 with `.localhost` names. `service install` accepts proxy options including `--port`, `--no-tls`, `--lan`, `--ip`, `--tld`, `--wildcard`, `--cert`, and `--key`. Use `--state-dir &lt;path&gt;` or `PORTLESS_STATE_DIR=&lt;path&gt;` to choose where service state and logs are written.

The chosen service configuration is written into launchd, systemd, or Task Scheduler and reused after reboot. `portless service status` reports the installed port, HTTPS mode, TLDs, LAN mode, wildcard mode, and state directory. macOS and Linux install a root-owned service so port 443 can bind at boot. Windows installs a Task Scheduler startup task that runs as SYSTEM. Installation and removal may require administrator privileges. `portless clean` automatically removes the service.

## CLI Reference

| Command                                           | Description                                                    |
| ------------------------------------------------- | -------------------------------------------------------------- |
| `portless`                                        | Run dev script through proxy                                   |
| `portless`                                        | From monorepo root: run all workspace packages                 |
| `portless --script &lt;name&gt;`                        | Run a specific package.json script (default: dev)              |
| `portless run [cmd] [args...]`                    | Infer name from project, run through proxy (auto-starts)       |
| `portless run --name &lt;name&gt; &lt;cmd&gt;`                | Override inferred base name (worktree prefix still applies)    |
| `portless &lt;name&gt; &lt;cmd&gt; [args...]`                 | Run app at `https://&lt;name&gt;.localhost` (auto-starts proxy)      |
| `portless get &lt;name&gt;`                             | Print URL for a service (for cross-service wiring)             |
| `portless get &lt;name&gt; --no-worktree`               | Print URL without worktree prefix                              |
| `portless list`                                   | Show active routes                                             |
| `portless doctor`                                 | Check proxy, routes, DNS, CA trust, and LAN prerequisites      |
| `portless trust`                                  | Add local CA to system trust store (for HTTPS)                 |
| `portless clean`                                  | Remove state, CA trust entry, and /etc/hosts block             |
| `portless prune`                                  | Kill orphaned dev servers from crashed sessions                |
| `portless prune --force`                          | Kill orphans with SIGKILL instead of SIGTERM                   |
| `portless proxy start`                            | Start HTTPS proxy as a daemon (port 443, auto-elevates)        |
| `portless proxy start --no-tls`                   | Start without HTTPS (plain HTTP on port 80)                    |
| `portless proxy start --lan`                      | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |
| `portless proxy start -p &lt;number&gt;`                | Start the proxy on a custom port                               |
| `portless proxy start --tld test`                 | Use .test instead of .localhost                                |
| `portless proxy start --tld localhost --tld test` | Serve both TLDs from one proxy                                 |
| `portless proxy start --tld dev.example.com`      | Use a multi-segment TLD for production-parity URLs             |
| `portless proxy start --foreground`               | Start the proxy in foreground (for debugging)                  |
| `portless proxy start --wildcard`                 | Allow unregistered subdomains to fall back to parent route     |
| `portless proxy stop`                             | Stop the proxy                                                 |
| `portless service install`                        | Start the HTTPS proxy when the OS starts                       |
| `portless service install --lan`                  | Start the service in LAN mode                                  |
| `portless service install --wildcard`             | Persist wildcard routing in the startup service                |
| `portless service status`                         | Show service and proxy status                                  |
| `portless service uninstall`                      | Remove the startup service                                     |
| `portless alias &lt;name&gt; &lt;port&gt;`                    | Register a static route (e.g. for Docker containers)           |
| `portless alias &lt;name&gt; &lt;port&gt; --force`            | Overwrite an existing route                                    |
| `portless alias --remove &lt;name&gt;`                  | Remove a static route                                          |
| `portless hosts sync`                             | Add routes to /etc/hosts (fixes Safari)                        |
| `portless hosts clean`                            | Remove portless entries from /etc/hosts                        |
| `portless &lt;name&gt; --app-port &lt;n&gt; &lt;cmd&gt;`            | Use a fixed port for the app instead of auto-assignment        |
| `portless &lt;name&gt; --tailscale &lt;cmd&gt;`               | Share the app on your Tailscale network (tailnet)              |
| `portless &lt;name&gt; --funnel &lt;cmd&gt;`                  | Share the app publicly via Tailscale Funnel                    |
| `portless &lt;name&gt; --ngrok &lt;cmd&gt;`                   | Share the app publicly via ngrok                               |
| `portless &lt;name&gt; --force &lt;cmd&gt;`                   | Kill the existing process and take over its route              |
| `portless --name &lt;name&gt; &lt;cmd&gt;`                    | Force `&lt;name&gt;` as app name (bypasses subcommand dispatch)      |
| `portless &lt;name&gt; -- &lt;cmd&gt; [args...]`              | Stop flag parsing; everything after `--` is passed to child    |
| `portless --help` / `-h`                          | Show help                                                      |
| `portless run --help`                             | Show help for a subcommand (also: alias, hosts, clean)         |
| `portless --version` / `-v`                       | Show version                                                   |

**Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `doctor`, `trust`, `clean`, `prune`, `proxy`, and `service` are subcommands and cannot be used as app names directly. Use `portless run &lt;cmd&gt;` to infer the name, or `portless --name &lt;name&gt; &lt;cmd&gt;` to force any name including reserved ones.

## portless.json

Optional config file. Portless looks for it in the current directory.

| Field     | Type    | Default                    | Description                                              |
| --------- | ------- | -------------------------- | -------------------------------------------------------- |
| `name`    | string  | inferred from package.json | Base app name (worktree prefix still applies)            |
| `script`  | string  | `"dev"`                    | Name of a package.json script to run                     |
| `appPort` | number  | auto-assigned              | Fixed port for the child process                         |
| `proxy`   | boolean | auto-detected              | Whether to route through the proxy (`false` for tasks)   |
| `apps`    | object  |                            | Overrides for workspace packages, keyed by relative path |
| `turbo`   | boolean | `true`                     | Set `false` to use direct spawning instead of turborepo  |

Each `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.

### package.json "portless" key

Instead of a separate `portless.json`, you can add a `"portless"` key to your `package.json`. A string value is shorthand for setting the name:

```json
{ "portless": "myapp" }
```

An object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):

```json
{ "portless": { "name": "myapp", "script": "dev:app" } }
```

Precedence (closest wins): CLI flags > package.json `"portless"` key > portless.json app entry > defaults.

## Troubleshooting

### Run diagnostics

Use `portless doctor` first when local routing or HTTPS behavior looks wrong. It is read-only and checks Node.js, state directory permissions, proxy liveness, route entries, hostname resolution, local CA trust, and LAN mode prerequisites.

### Proxy not running

The proxy auto-starts when you run an app with `portless &lt;name&gt; &lt;cmd&gt;`. If it doesn't start (e.g. port conflict), start it manually:

```bash
portless proxy start
```

### Port already in use

Another process is bound to the proxy port. Either stop it first, or use a different port:

```bash
portless proxy start -p 8080
```

### Framework not respecting PORT

Portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag for frameworks that ignore the `PORT` env var: **Vite**, **VitePlus** (`vp`), **Astro**, **React Router**, **Angular**, **Expo**, and **React Native**. SvelteKit uses Vite internally and is handled automatically. Injection reaches through a package script whose command starts with the framework or a known runner, and only for the framework's server commands (`dev`, `serve`, `preview`, `start`, or a bare `vite`) — `vite build`, `vite optimize`, `vp test` and other non-serving commands reject the flags, so they are left untouched, as is any invocation portless cannot classify (`vp --mode dev build`). It is also skipped for a compound command (`&&`, `|`, `;`), a trailing `#` comment, its own `--` option terminator, an env prefix (`NODE_ENV=production vite`), delegation to another script, and runner flags before the script name (`bun run --bun dev`) — each of those keeps its own port and the app returns 502, so set the port in the script yourself.

For other frameworks that don't read `PORT`, pass the port manually:

- **Webpack Dev Server**: use `--port $PORT`
- **Custom servers**: read `process.env.PORT` and listen on it

### Permission errors

The default ports (80 for HTTP, 443 for HTTPS) require `sudo` on macOS and Linux. Portless auto-elevates with sudo when needed. If sudo is unavailable, it falls back to port 1355 (no sudo needed). On Windows, no elevation is required.

```bash
portless proxy start --https           # Auto-elevates with sudo for port 443
portless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)
portless proxy stop                    # Stop (use sudo if started with sudo)
```

### Safari can't find .localhost URLs

Safari relies on the system DNS resolver for `.localhost` subdomains, which may not resolve them on all macOS configurations. Chrome, Firefox, and Edge have built-in handling.

Fix:

```bash
portless hosts sync    # Adds current routes to /etc/hosts
portless hosts clean   # Remove entries later
```

Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable.

### Browser shows certificate warning with --https

The local CA may not be trusted yet. Run:

```bash
portless trust
```

This adds the portless local CA to your system trust store. After that, restart the browser.

### Remove portless from the machine

```bash
portless clean
```

Stops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux. If trust-store removal fails, portless retains its CA certificate and key so a later `portless clean` can safely retry.

### Proxy loop (508 Loop Detected)

If your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop.

Fix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.):

```ts
// vite.config.ts
proxy: {
  "/api": {
    target: "https://api.myapp.localhost",
    changeOrigin: true,
    ws: true,
  },
}
```

Portless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=~/.portless/ca.pem`. Alternatively, use `--no-tls` for plain HTTP.

### Tailscale not working

If `--tailscale` or `--funnel` fails:

```bash
tailscale status     # Check if connected
tailscale up         # Connect to your tailnet
```

Requires the Tailscale CLI to be installed (https://tailscale.com/download) and on PATH.

### ngrok not working

If `--ngrok` fails:

```bash
ngrok version                         # Check if installed
ngrok config add-authtoken <token>    # Configure authentication
```

Requires the ngrok CLI to be installed (https://ngrok.com/download) and on PATH.

### Requirements

- Node.js 24+
- macOS, Linux, or Windows
- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows)
- `tailscale` CLI (optional, for `--tailscale` and `--funnel`)
- `ngrok` CLI (optional, for `--ngrok`)



---

## Checklists (1)

### New Project Setup

# New Project Setup Checklist

Add portless to an existing or new project in 7 steps.

## Prerequisites

- `portless` CLI installed (`npm i -g portless` or `brew install portless`)
- Project runs locally with a dev server

## Steps

### 1. Choose Name(s)

Pick portless names for your services. Conventions:

| Pattern | Example | When to use |
|---------|---------|-------------|
| `&lt;project&gt;` | `myapp` | Single-service project |
| `&lt;project&gt;-&lt;layer&gt;` | `myapp-api`, `myapp-web` | Multi-service / monorepo |
| `&lt;service&gt;` | `api`, `web`, `docs` | When project context is obvious |

Names become `https://&lt;name&gt;.localhost`. Keep them short.

### 2. Update Dev Scripts

Wrap your dev commands with `portless run`:

```jsonc
// package.json
{
  "scripts": {
    // Before
    "dev": "next dev",
    // After
    "dev": "portless run --name myapp next dev"
  }
}
```

For frameworks that don't auto-detect `PORT` (FastAPI, Django), add explicit flags. See `references/framework-integration.md` for per-framework recipes.

### 3. Update Environment Variables

Replace hardcoded `localhost:NNNN` URLs in `.env` files:

```bash
# Before
API_URL=http://localhost:3001
NEXT_PUBLIC_API_URL=http://localhost:3001

# After
API_URL=https://api.localhost
NEXT_PUBLIC_API_URL=https://api.localhost
```

Search for stale references:

```bash
grep -rn 'localhost:[0-9]' --include='*.env*' --include='*.ts' --include='*.js' .
```

### 4. Update Proxy Configs

If your frontend proxies API calls, update the target and add `changeOrigin: true`:

**Next.js** (`next.config.js`):
```js
rewrites: () => [{
  source: "/api/:path*",
  destination: "https://api.localhost/:path*",
  changeOrigin: true,
}]
```

**Vite** (`vite.config.ts`):
```js
server: {
  proxy: {
    "/api": { target: "https://api.localhost", changeOrigin: true, secure: false }
  }
}
```

### 5. CI Compatibility

Portless is a dev-only tool. Disable it in CI:

```yaml
# GitHub Actions
env:
  PORTLESS: "0"

# Or in package.json
"dev:ci": "PORTLESS=0 next dev"
```

### 6. Monorepo Pattern

For Turborepo / Nx workspaces, configure per-package:

```jsonc
// apps/web/package.json
{ "scripts": { "dev": "portless run --name web next dev" } }

// apps/api/package.json
{ "scripts": { "dev": "portless run --name api uvicorn main:app --port $PORT --host $HOST" } }

// Root — runs both via turbo
// turbo dev → starts web.localhost + api.localhost
```

### 7. Verify

```bash
# List active portless services
portless list

# Open in browser
open https://myapp.localhost

# Test cross-service calls
curl https://api.localhost/health
```

Check that:
- [ ] Dev server starts at `https://&lt;name&gt;.localhost`
- [ ] Cross-service calls resolve (no `ECONNREFUSED`)
- [ ] OAuth callbacks work with new URLs (if applicable)
- [ ] CI still works with `PORTLESS=0`

## Rollback

If something breaks, remove `portless run --name &lt;name&gt;` from scripts and revert `.env` changes. Portless is additive — removing it just falls back to direct `localhost:PORT`.
