vercel-labs/portlessPublic

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

AI summary: A zero-config local reverse proxy that provides clean, HTTPS-enabled `.localhost` domains for development servers.

Stars
10.3K
+12 today
Forks
338
Watchers
20
Open issues
63
Open PRs
53
Contributors
~22
Commits
152
Branches
123

TypeScriptApache-2.0Created Feb 15, 2026Last push 8d agoLatest release v0.15.4+69 stars this week+69 this month

Star history

since Feb 15, 2026
05K10KFeb 2026Apr 2026Jun 2026Aug 2026
10.3K stars as of Aug 6, 2026, tracked back to Feb 15, 2026. Historical curve reconstructed from public GitHub event archives, calibrated to the current total.

Contribution activity

commits per day, last 52 weeks
AugSepOctNovDecJanFebMarAprMayJunJulMonWedFri2025-08-02: 0 commits2025-08-03: 0 commits2025-08-04: 0 commits2025-08-05: 0 commits2025-08-06: 0 commits2025-08-07: 0 commits2025-08-08: 0 commits2025-08-09: 0 commits2025-08-10: 0 commits2025-08-11: 0 commits2025-08-12: 0 commits2025-08-13: 0 commits2025-08-14: 0 commits2025-08-15: 0 commits2025-08-16: 0 commits2025-08-17: 0 commits2025-08-18: 0 commits2025-08-19: 0 commits2025-08-20: 0 commits2025-08-21: 0 commits2025-08-22: 0 commits2025-08-23: 0 commits2025-08-24: 0 commits2025-08-25: 0 commits2025-08-26: 0 commits2025-08-27: 0 commits2025-08-28: 0 commits2025-08-29: 0 commits2025-08-30: 0 commits2025-08-31: 0 commits2025-09-01: 0 commits2025-09-02: 0 commits2025-09-03: 0 commits2025-09-04: 0 commits2025-09-05: 0 commits2025-09-06: 0 commits2025-09-07: 0 commits2025-09-08: 0 commits2025-09-09: 0 commits2025-09-10: 0 commits2025-09-11: 0 commits2025-09-12: 0 commits2025-09-13: 0 commits2025-09-14: 0 commits2025-09-15: 0 commits2025-09-16: 0 commits2025-09-17: 0 commits2025-09-18: 0 commits2025-09-19: 0 commits2025-09-20: 0 commits2025-09-21: 0 commits2025-09-22: 0 commits2025-09-23: 0 commits2025-09-24: 0 commits2025-09-25: 0 commits2025-09-26: 0 commits2025-09-27: 0 commits2025-09-28: 0 commits2025-09-29: 0 commits2025-09-30: 0 commits2025-10-01: 0 commits2025-10-02: 0 commits2025-10-03: 0 commits2025-10-04: 0 commits2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 0 commits2026-02-03: 0 commits2026-02-04: 0 commits2026-02-05: 0 commits2026-02-06: 0 commits2026-02-07: 0 commits2026-02-08: 0 commits2026-02-09: 0 commits2026-02-10: 0 commits2026-02-11: 0 commits2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 0 commits2026-02-15: 10 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 2 commits2026-02-19: 3 commits2026-02-20: 0 commits2026-02-21: 0 commits2026-02-22: 2 commits2026-02-23: 0 commits2026-02-24: 2 commits2026-02-25: 5 commits2026-02-26: 0 commits2026-02-27: 0 commits2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 7 commits2026-03-04: 12 commits2026-03-05: 0 commits2026-03-06: 2 commits2026-03-07: 0 commits2026-03-08: 0 commits2026-03-09: 0 commits2026-03-10: 4 commits2026-03-11: 1 commit2026-03-12: 0 commits2026-03-13: 0 commits2026-03-14: 2 commits2026-03-15: 3 commits2026-03-16: 3 commits2026-03-17: 8 commits2026-03-18: 0 commits2026-03-19: 4 commits2026-03-20: 1 commit2026-03-21: 5 commits2026-03-22: 0 commits2026-03-23: 1 commit2026-03-24: 0 commits2026-03-25: 0 commits2026-03-26: 0 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 0 commits2026-03-30: 1 commit2026-03-31: 9 commits2026-04-01: 8 commits2026-04-02: 4 commits2026-04-03: 0 commits2026-04-04: 5 commits2026-04-05: 0 commits2026-04-06: 0 commits2026-04-07: 5 commits2026-04-08: 0 commits2026-04-09: 1 commit2026-04-10: 0 commits2026-04-11: 1 commit2026-04-12: 0 commits2026-04-13: 6 commits2026-04-14: 0 commits2026-04-15: 2 commits2026-04-16: 0 commits2026-04-17: 0 commits2026-04-18: 0 commits2026-04-19: 0 commits2026-04-20: 1 commit2026-04-21: 0 commits2026-04-22: 0 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 2 commits2026-04-27: 3 commits2026-04-28: 3 commits2026-04-29: 1 commit2026-04-30: 0 commits2026-05-01: 0 commits2026-05-02: 0 commits2026-05-03: 0 commits2026-05-04: 0 commits2026-05-05: 0 commits2026-05-06: 1 commit2026-05-07: 2 commits2026-05-08: 0 commits2026-05-09: 0 commits2026-05-10: 0 commits2026-05-11: 0 commits2026-05-12: 0 commits2026-05-13: 0 commits2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 0 commits2026-05-18: 0 commits2026-05-19: 0 commits2026-05-20: 1 commit2026-05-21: 0 commits2026-05-22: 0 commits2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 0 commits2026-05-26: 0 commits2026-05-27: 2 commits2026-05-28: 0 commits2026-05-29: 0 commits2026-05-30: 0 commits2026-05-31: 0 commits2026-06-01: 0 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 2 commits2026-06-05: 0 commits2026-06-06: 0 commits2026-06-07: 0 commits2026-06-08: 0 commits2026-06-09: 0 commits2026-06-10: 1 commit2026-06-11: 0 commits2026-06-12: 0 commits2026-06-13: 0 commits2026-06-14: 0 commits2026-06-15: 0 commits2026-06-16: 0 commits2026-06-17: 0 commits2026-06-18: 0 commits2026-06-19: 0 commits2026-06-20: 0 commits2026-06-21: 0 commits2026-06-22: 0 commits2026-06-23: 0 commits2026-06-24: 2 commits2026-06-25: 0 commits2026-06-26: 0 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 0 commits2026-06-30: 2 commits2026-07-01: 0 commits2026-07-02: 0 commits2026-07-03: 0 commits2026-07-04: 0 commits2026-07-05: 0 commits2026-07-06: 0 commits2026-07-07: 0 commits2026-07-08: 0 commits2026-07-09: 0 commits2026-07-10: 1 commit2026-07-11: 0 commits2026-07-12: 0 commits2026-07-13: 3 commits2026-07-14: 2 commits2026-07-15: 0 commits2026-07-16: 2 commits2026-07-17: 1 commit2026-07-18: 0 commits2026-07-19: 0 commits2026-07-20: 0 commits2026-07-21: 0 commits2026-07-22: 1 commit2026-07-23: 0 commits2026-07-24: 0 commits2026-07-25: 0 commits2026-07-26: 0 commits2026-07-27: 0 commits2026-07-28: 0 commits2026-07-29: 0 commits2026-07-30: 0 commits2026-07-31: 0 commits2026-08-01: 0 commits
152 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Widely adopted

    10,337 stars

  • Permissive license

    Apache-2.0

  • Continuous integration

    Automated checks passing

What portless does

Portless automatically manages port assignment and reverse proxy routing for local development environments. Instead of accessing apps via `localhost:3000` or `localhost:8080`, it intercepts traffic and routes it to dynamically assigned ports based on app names, creating clean URLs like `https://myapp.localhost`. It automatically provisions a local Certificate Authority (CA) to enable seamless HTTPS and HTTP/2 multiplexing without browser warnings. Furthermore, it natively supports monorepos, TurboRepo, and Git worktrees by parsing workspace definitions and automatically generating unique subdomains for different branches.

Portless is designed for web developers, particularly those managing complex full-stack applications or monorepos, who want to streamline their local development experience by eliminating port conflicts and manual HTTPS configuration.

  • Zero-config HTTPS: Automatically generates and trusts a local CA to serve development apps over HTTPS with HTTP/2 multiplexing.
  • Dynamic port allocation: Spawns development scripts on free ports and automatically routes named `.localhost` domains to them.
  • Monorepo auto-discovery: Reads pnpm, yarn, and npm workspace files to automatically expose all local packages via subdomains.
  • Git worktree isolation: Detects linked git worktrees and prepends branch names as subdomains to prevent port collisions across checkouts.
  • Custom TLDs: Supports overriding the default `.localhost` extension with custom domains like `.test` or `.dev.example.com`.

Where teams use it

Local monorepo development

Full-stack teams use Portless to easily route requests between locally running frontend and backend workspace packages without hardcoding port numbers.

Secure OAuth testing

Developers use custom TLDs and auto-generated HTTPS certificates to satisfy strict redirect URI requirements from providers like Google or Apple.

Concurrent branch testing

Engineers working on multiple features simultaneously use git worktrees to run different app versions side-by-side on unique subdomains.

Simplifying team onboarding

Project maintainers configure a `portless.json` file so new contributors can spin up complex architectures simply by running `portless`.

Getting started: npm install -g portless portless myapp npm run dev

README

main branch

portless

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

- "dev": "next dev"                  # http://localhost:3000
+ "dev": "portless run next dev"     # https://myapp.localhost

Install

Global (recommended):

npm install -g portless

Or as a project dev dependency:

npm install -D portless

portless is pre-1.0. When installed per-project, different contributors may run different versions. The state directory format may change between releases, which can require re-running portless trust.

Run your app

portless myapp next dev
# -> https://myapp.localhost

HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use --no-tls for plain HTTP.

The proxy auto-starts when you run an app. A random port (4000-4999) is assigned via the PORT environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right --port flag and, when needed, a matching --host flag.

When auto-starting, portless 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 (PORTLESS_PORT, PORTLESS_HTTPS, etc.) always take priority.

Portless stores per-user state in ~/.portless. When the proxy runs under sudo, it resolves this path from the invoking user's home so the proxy and unprivileged app processes share the same route registrations.

In non-interactive environments (no TTY, or CI=1), portless exits with a descriptive error instead of prompting, so task runners like turborepo and CI scripts fail early with a clear message.

Configuration

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:

portless        # -> runs "dev" script, https://<project>.localhost

Use an optional portless.json to override defaults:

{ "name": "myapp" }
portless        # -> runs "dev" script, https://myapp.localhost

The script defaults to "dev". The name is inferred from package.json if not set in config.

Monorepo

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

{
  "apps": {
    "apps/web": { "name": "myapp" },
    "apps/api": { "name": "api.myapp" }
  }
}
portless        # from repo root: starts all workspace packages with a "dev" script
cd apps/web && portless   # start just one package

The apps map is optional and only needed for name overrides. Packages not listed still auto-discover with names inferred from their package.json.

Without an apps map, hostnames follow the <package>.<project>.localhost convention. The project name comes from the most common npm scope across workspace packages (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 gets the bare <project>.localhost without duplication.

Config fields

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

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:

{
  "name": "@myorg/web",
  "portless": "myapp"
}

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

{
  "name": "@myorg/web",
  "portless": { "name": "myapp", "script": "dev:app" }
}

The package.json "portless" key takes precedence over portless.json app entries but is overridden by CLI flags.

--script flag

Override the default script for a single invocation:

portless --script start       # run "start" instead of "dev"
portless --script test        # run "test" instead of "dev"

Turborepo

To use portless with turborepo, put portless as the dev script and the real command in a separate script:

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

Turbo runs each package's dev script, which invokes portless. Portless reads the config, detects the package manager, and runs pnpm run dev:app (or yarn/bun/npm) through the proxy. No changes to turbo.json are needed.

pnpm dev at the root works through turbo as usual. People without portless can run pnpm run dev:app directly.

Use in package.json

You can still use portless in package.json scripts:

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

With a portless.json, you can simplify to:

{
  "scripts": {
    "dev": "next dev"
  }
}

Then run portless or portless run to go through the proxy.

Subdomains

Organize services with subdomains:

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). Use --wildcard when starting the proxy to allow any subdomain of a registered route to fall back to that app (e.g. tenant1.myapp.localhost routes to the myapp app without extra registration).

Git Worktrees

portless run automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain so each worktree gets its own URL without any config changes:

# 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

Use --name to override the inferred base name while keeping the worktree prefix:

portless run --name myapp next dev   # -> https://fix-ui.myapp.localhost

Put portless run in your package.json once and it works everywhere. The main checkout uses the plain name, each worktree gets a unique subdomain. No collisions, no --force.

Custom TLD

By default, portless uses .localhost which auto-resolves to 127.0.0.1 in most browsers. If you prefer a different TLD (e.g. .test), use --tld:

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

The proxy auto-syncs /etc/hosts for route hostnames (including .test), so those domains resolve on your machine.

Repeat --tld to serve the same app names under multiple TLDs from one proxy:

portless proxy start --tld localhost --tld test
portless myapp next dev
# -> https://myapp.localhost
# -> https://myapp.test

When multiple TLDs are configured, PORTLESS_URL uses the first TLD. PORTLESS_TLD also accepts a comma separated list, e.g. PORTLESS_TLD=localhost,test.

Recommended: .test (IANA-reserved, no collision risk). Avoid .local (conflicts with mDNS/Bonjour) and .dev (Google-owned, forces HTTPS via HSTS).

Multi-segment TLDs

The --tld value accepts a lowercase DNS name (one or more dot-separated labels, no trailing dot), so a domain you own can be used as the "TLD". This gives local URLs the same structure as production, which keeps OAuth redirect URIs, cross-subdomain cookies, and host-based routing working the same way in both environments:

portless proxy start --tld dev.example.com
portless myapp next dev
# -> https://myapp.dev.example.com

Each label must follow DNS rules: lowercase letters, digits, and interior hyphens, with at most 63 characters per label and 253 characters total. The full hostname (app.TLD) is also subject to the 253-character DNS limit.

The proxy auto-syncs /etc/hosts for registered hostnames, so myapp.dev.example.com resolves to 127.0.0.1 on your machine. This is a loopback-only setup: outside LAN mode the proxy binds only to 127.0.0.1 and ::1 (see below), so a custom TLD is reachable only from the machine running the proxy. Reaching the proxy from other devices requires LAN mode (--lan), but LAN mode serves apps under the .local TLD and ignores a custom --tld, so the two cannot be combined today.

Strict OAuth providers (Google, Apple) reject .localhost and .test redirect URIs but accept a real domain, so https://myapp.dev.example.com/api/auth/callback/google works as a redirect URI.

How it works

flowchart TD
    Browser["Browser<br>myapp.localhost"]
    Proxy["portless proxy<br>(port 80 or 443)"]
    App1[":4123<br>myapp"]
    App2[":4567<br>api"]

    Browser --> Proxy
    Proxy --> App1
    Proxy --> App2
Loading
  1. Start the proxy: auto-starts when you run an app, or start explicitly with portless proxy start
  2. Run apps: portless <name> <command> assigns a free port and registers with the proxy
  3. Access via URL: https://<name>.localhost routes through the proxy to your app

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.

HTTP/2 + HTTPS

HTTPS with HTTP/2 is enabled by default. Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers that serve many unbundled files (Vite, Nuxt, etc.). HTTP/2 multiplexes all requests over a single connection.

WebSockets work over both protocol versions, so dev server HMR (Next.js, Vite, etc.) works through the proxy: HTTP/1.1 Upgrade requests are forwarded as-is, and WebSockets opened over an HTTP/2 connection use extended CONNECT (RFC 8441).

On first run, portless generates a local CA and adds it to your system trust store. No browser warnings. No manual setup.

# Use your own certs (e.g., from mkcert)
portless proxy start --cert ./cert.pem --key ./key.pem

# Disable HTTPS (plain HTTP on port 80)
portless proxy start --no-tls

# If you skipped the trust prompt on first run, trust the CA later
portless trust

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.

Start at OS startup

Install the proxy as an OS startup service so clean HTTPS URLs are available after reboot without starting the proxy from a terminal:

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 the proxy options you would use with proxy start, including --port, --no-tls, --lan, --ip, --tld, --wildcard, --cert, and --key. Use --state-dir <path> or PORTLESS_STATE_DIR=<path> 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.

LAN mode

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 switches to mDNS discovery. This makes services available as <name>.local to devices on the same network. Portless auto-detects your LAN IP and follows Wi-Fi/IP changes automatically, but you can pin another address with --ip <address> or by exporting PORTLESS_LAN_IP. Set PORTLESS_LAN=1 in your shell (0/1 boolean) to make LAN mode the default whenever the proxy starts.

Portless remembers LAN mode via proxy.lan, so if you stop a LAN proxy and start it 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 tools that portless already spawns: macOS ships with dns-sd, while Linux uses avahi-publish-address from avahi-utils (install via sudo apt install avahi-utils or your distro’s equivalent). If the command is missing or your network isn’t reachable, portless proxy start --lan prints the relevant error and exits.

Framework notes

  • Next.js: add your .local hostnames to allowedDevOrigins:

    // 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 your dev server with teammates on your Tailscale network:

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

Each --tailscale app is root-mounted on its own Tailscale HTTPS port, so no framework basePath configuration is needed. The first app gets port 443, subsequent apps get 8443, 8444, etc.

portless myapp --tailscale next dev     # -> https://devbox.ts.net
portless api --tailscale pnpm start     # -> https://devbox.ts.net:8443

Use --funnel to expose your dev server to the public internet via Tailscale Funnel:

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

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.

Set PORTLESS_TAILSCALE=1 in your shell profile or .env to share every app by default. portless list shows both local and tailnet URLs. Tailscale serve registrations are cleaned up automatically when the app exits.

Requires the Tailscale CLI to be installed and connected (tailscale up), with Tailscale HTTPS certificates enabled.

ngrok sharing

Expose your dev server to the public internet with ngrok:

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

Set PORTLESS_NGROK=1 in your shell profile or .env to enable ngrok by default when portless runs an app. portless list shows both local and ngrok URLs. The ngrok tunnel is cleaned up automatically when the app exits.

Requires the ngrok CLI to be installed and authenticated. If ngrok reports an authentication error, run ngrok config add-authtoken <token> and try again.

Commands

portless                        # Run dev script through proxy
portless                        # From monorepo root: run all workspace packages
portless run [--name <name>] [cmd] [args...]  # Infer name, run through proxy
portless <name> <cmd> [args...]  # Run app at https://<name>.localhost
portless alias <name> <port>     # Register a static route (e.g. for Docker)
portless alias <name> <port> --force  # Overwrite an existing route
portless alias --remove <name>   # Remove a static route
portless list                    # Show active routes
portless doctor                  # Check proxy, routes, DNS, and CA trust
portless trust                   # Add local CA to system trust store
portless clean                   # Remove state, CA trust entry, and hosts block
portless prune                   # Kill orphaned dev servers from crashed sessions
portless hosts sync              # Add routes to /etc/hosts (fixes Safari)
portless hosts clean             # Remove portless entries from /etc/hosts

# Disable portless (run command directly)
PORTLESS=0 pnpm dev              # Bypasses proxy, uses default port

# Proxy control
portless proxy start             # Start the HTTPS proxy (port 443, daemon)
portless proxy start --no-tls    # Start without HTTPS (port 80)
portless proxy start --lan       # Start in LAN mode (mDNS .local for devices)
portless proxy start -p 1355     # Start on a custom port (no sudo)
portless proxy start --foreground  # Start in foreground (for debugging)
portless proxy start --wildcard  # Allow unregistered subdomains to fall back to parent
portless proxy stop              # Stop the proxy

# OS startup service
portless service install         # Start HTTPS proxy when the OS starts
portless service install --lan   # Start service in LAN mode
portless service install --wildcard  # Persist wildcard routing in the service
portless service status          # Show service and proxy status
portless service uninstall       # Remove the startup service

Options

-p, --port <number>              Port for the proxy (default: 443, or 80 with --no-tls)
--no-tls                         Disable HTTPS (use plain HTTP on port 80)
--https                          Enable HTTPS (default, accepted for compatibility)
--lan                            Enable LAN mode (mDNS .local for real devices)
--ip <address>                   Pin a specific LAN IP (disables auto-follow; use with --lan)
--cert <path>                    Use a custom TLS certificate
--key <path>                     Use a custom TLS private key
--foreground                     Run proxy in foreground instead of daemon
--tld <tld>                      Use a custom TLD instead of .localhost; repeat for more
--wildcard                       Allow unregistered subdomains to fall back to parent route
--state-dir <path>               Use a custom state directory with service install
--script <name>                  Run a specific package.json script (default: dev)
--app-port <number>              Use a fixed port for the app (skip auto-assignment)
--tailscale                      Share the app on your Tailscale network (tailnet)
--funnel                         Share the app publicly via Tailscale Funnel
--ngrok                          Share the app publicly via ngrok
--force                          Kill the existing process and take over its route
--name <name>                    Use <name> as the app name

Environment variables

# Configuration
PORTLESS_PORT=<number>           Override the default proxy port
PORTLESS_APP_PORT=<number>       Use a fixed port for the app (same as --app-port)
PORTLESS_HTTPS=0                 Disable HTTPS (same as --no-tls)
PORTLESS_LAN=1                   Enable LAN mode when set to 1 (auto-detects LAN IP)
PORTLESS_LAN_IP=<address>        Pin a specific LAN IP for LAN mode
PORTLESS_TLD=<tld>[,<tld>]       Use one or more TLDs (e.g. localhost,test)
PORTLESS_WILDCARD=1              Allow unregistered subdomains to fall back to parent route
PORTLESS_SYNC_HOSTS=0            Disable auto-sync of /etc/hosts (on by default)
PORTLESS_TAILSCALE=1             Share apps on your Tailscale network (same as --tailscale)
PORTLESS_FUNNEL=1                Share apps publicly via Tailscale Funnel (same as --funnel)
PORTLESS_NGROK=1                 Share apps publicly via ngrok (same as --ngrok)
PORTLESS_STATE_DIR=<path>        Override the state directory

# Injected into child processes
PORT                             Ephemeral port the child should listen on
HOST                             Usually 127.0.0.1 (omitted for Expo in LAN mode)
PORTLESS_URL                     Primary public URL (e.g. https://myapp.localhost)
PORTLESS_TAILSCALE_URL           Tailscale URL of the app (when --tailscale is active)
PORTLESS_NGROK_URL               ngrok URL of the app (when --ngrok is active)
NODE_EXTRA_CA_CERTS              Path to the portless CA (when HTTPS is active)

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 <cmd> to infer the name from your project, or portless --name <name> <cmd> to force any name including reserved ones.

Uninstall / reset

To remove portless data from your machine (proxy state under ~/.portless and the system state directory, the local CA from the OS trust store when portless installed it, and the portless block in /etc/hosts):

portless clean

macOS/Linux may prompt for sudo. Custom certificate paths passed with --cert and --key are not deleted. If trust-store removal fails, portless retains its CA certificate and key so a later portless clean can safely retry.

Safari / DNS

.localhost subdomains auto-resolve to 127.0.0.1 in Chrome, Firefox, and Edge. Safari relies on the system DNS resolver, which may not handle .localhost subdomains on all configurations.

If Safari can't find your .localhost URL:

portless hosts sync    # Add current routes to /etc/hosts
portless hosts clean   # Clean up later

Auto-syncs /etc/hosts for route hostnames by default (.localhost, custom TLDs, LAN .local). Set PORTLESS_SYNC_HOSTS=0 to disable.

Troubleshooting

Run portless doctor to inspect local health without changing state. It checks Node.js, the state directory, proxy liveness, route entries, HTTPS CA trust, hostname resolution, and LAN mode prerequisites, then prints suggested fixes.

Proxying Between Portless Apps

If your frontend dev server (e.g. Vite, webpack) proxies API requests to another portless app, make sure the proxy rewrites the Host header. Without this, portless routes the request back to the frontend in an infinite loop.

Vite (vite.config.ts):

server: {
  proxy: {
    "/api": {
      target: "https://api.myapp.localhost",
      changeOrigin: true,
      ws: true,
    },
  },
}

webpack-dev-server (webpack.config.js):

devServer: {
  proxy: [{
    context: ["/api"],
    target: "https://api.myapp.localhost",
    changeOrigin: 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.

Portless detects this misconfiguration and responds with 508 Loop Detected along with a message pointing to this fix.

Development

This repo is a pnpm workspace monorepo using Turborepo. The publishable package lives in packages/portless/.

Use Node.js 24+ and pnpm 11 for repository development. The .node-version file pins the Node major for version managers.

pnpm install          # Install all dependencies
pnpm build            # Build all packages
pnpm test             # Run tests
pnpm test:coverage    # Run tests with coverage
pnpm lint             # Lint all packages
pnpm type-check       # Type-check all packages
pnpm format           # Format all files with Prettier

Requirements

  • Node.js 24+
  • macOS, Linux, or Windows
  • Tailscale CLI (optional, for --tailscale and --funnel)
  • ngrok CLI (optional, for --ngrok)
View on GitHub

Recent activity

commits and pull requests

Releases and announcements

38 total
  1. v0.15.4v0.15.4Jul 16, 2026

    ### Bug Fixes - **Loopback-only proxy binding**: Outside LAN mode, the proxy and HTTP redirect listeners now bind only to `127.0.0.1` and `::1`, so Portless routes cannot be reached through LAN, VPN, or other network interfaces. LAN mode still binds to all interfaces explicitly. (#361) ### Contributors - @ctate

  2. v0.15.3v0.15.3Jul 14, 2026

    ### Bug Fixes - **State directory under sudo**: Portless now resolves per-user state from the original sudo user's home, so an elevated proxy and unprivileged app processes share the same routes instead of writing to separate state directories. (#357) - **Windows and WSL CA trust**: On WSL, `portless trust` now installs the local CA in both Linux and Windows trust stores, while `portless clean` removes the exact certificate from both. Failed trust-store cleanup preserves the CA identity for safe retries, including on native Windows. (#357) ### Contributors - @ctate - @gerardbalaoro

  3. v0.15.2v0.15.2Jul 14, 2026

    ### Bug Fixes - **Tailscale funnel routing**: Proxy now routes requests addressed to a route's Tailscale funnel or serve hostname, so `--funnel` and `--tailscale` apps reached at `<device>.ts.net` no longer return a 404, including when several apps share one hostname on different ports. (#352) - **IPv6-only dev servers return 502**: Proxy now dials upstreams over both loopback families, fixing 502s when a dev server binds `::1` only, such as Vite on Node 17+. (#353) - **Worktree prefix in multi-app mode**: Bare `portless` in a monorepo worktree now applies the branch prefix in multi-app mode as it already did for single apps, so hostnames no longer collide across worktrees. (#355) ### Contributors - @Railly - @ahfoysal

  4. v0.15.1v0.15.1Jun 30, 2026

    ### New Features - **Multi-TLD proxy support**: `--tld` is now repeatable and `PORTLESS_TLD` accepts comma separated values, so one proxy can serve the same app names across multiple TLDs. Routes, TLS, service state, hosts sync, framework environment, and workspace launches now use the full configured TLD list. (#344) ### Contributors - @ctate

  5. v0.15.0v0.15.0Jun 24, 2026

    ### New Features - **`portless doctor`**: New read only diagnostics command checks Node.js, the state directory, proxy liveness, route entries, hostname resolution, HTTPS CA trust, and LAN prerequisites, then prints suggested fixes. (#337) ### Bug Fixes - **HTTP/2 Host forwarding**: Proxy now forwards HTTP/2 `:authority` as `Host` to HTTP/1.1 backends, fixing apps that depend on Host and previously saw `127.0.0.1` for browser traffic. (#328) - **`--force` takeover cleanup**: Exit cleanup now removes only routes still owned by the exiting process, so a forced takeover does not deregister the new owner's route. (#328) ### Contributors - @ctate

Commits per week

last 52 weeks
270Week of 2025-08-02: 0 commitsWeek of 2025-08-09: 0 commitsWeek of 2025-08-16: 0 commitsWeek of 2025-08-23: 0 commitsWeek of 2025-08-30: 0 commitsWeek of 2025-09-06: 0 commitsWeek of 2025-09-13: 0 commitsWeek of 2025-09-20: 0 commitsWeek of 2025-09-27: 0 commitsWeek of 2025-10-04: 0 commitsWeek of 2025-10-11: 0 commitsWeek of 2025-10-18: 0 commitsWeek of 2025-10-25: 0 commitsWeek of 2025-11-01: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 0 commitsWeek of 2026-02-08: 0 commitsWeek of 2026-02-15: 15 commitsWeek of 2026-02-22: 9 commitsWeek of 2026-03-01: 21 commitsWeek of 2026-03-08: 7 commitsWeek of 2026-03-15: 24 commitsWeek of 2026-03-22: 1 commitsWeek of 2026-03-29: 27 commitsWeek of 2026-04-05: 7 commitsWeek of 2026-04-12: 8 commitsWeek of 2026-04-19: 1 commitsWeek of 2026-04-26: 9 commitsWeek of 2026-05-03: 3 commitsWeek of 2026-05-10: 0 commitsWeek of 2026-05-17: 1 commitsWeek of 2026-05-24: 2 commitsWeek of 2026-05-31: 2 commitsWeek of 2026-06-07: 1 commitsWeek of 2026-06-14: 0 commitsWeek of 2026-06-21: 2 commitsWeek of 2026-06-28: 2 commitsWeek of 2026-07-05: 1 commitsWeek of 2026-07-12: 8 commitsWeek of 2026-07-19: 1 commitsWeek of 2026-07-26: 0 commitsAug 2, 2025Jul 26, 2026
152 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 1 commitsSun 1:00 — 1 commitsSun 2:00 — 0 commitsSun 3:00 — 0 commitsSun 4:00 — 0 commitsSun 5:00 — 0 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 0 commitsSun 9:00 — 2 commitsSun 10:00 — 0 commitsSun 11:00 — 2 commitsSun 12:00 — 6 commitsSun 13:00 — 0 commitsSun 14:00 — 2 commitsSun 15:00 — 1 commitsSun 16:00 — 0 commitsSun 17:00 — 0 commitsSun 18:00 — 0 commitsSun 19:00 — 0 commitsSun 20:00 — 0 commitsSun 21:00 — 2 commitsSun 22:00 — 0 commitsSun 23:00 — 0 commitsMon 0:00 — 1 commitsMon 1:00 — 0 commitsMon 2:00 — 0 commitsMon 3:00 — 0 commitsMon 4:00 — 0 commitsMon 5:00 — 0 commitsMon 6:00 — 0 commitsMon 7:00 — 0 commitsMon 8:00 — 1 commitsMon 9:00 — 0 commitsMon 10:00 — 0 commitsMon 11:00 — 0 commitsMon 12:00 — 0 commitsMon 13:00 — 3 commitsMon 14:00 — 0 commitsMon 15:00 — 3 commitsMon 16:00 — 1 commitsMon 17:00 — 5 commitsMon 18:00 — 2 commitsMon 19:00 — 0 commitsMon 20:00 — 0 commitsMon 21:00 — 1 commitsMon 22:00 — 0 commitsMon 23:00 — 1 commitsTue 0:00 — 2 commitsTue 1:00 — 2 commitsTue 2:00 — 1 commitsTue 3:00 — 0 commitsTue 4:00 — 0 commitsTue 5:00 — 0 commitsTue 6:00 — 0 commitsTue 7:00 — 1 commitsTue 8:00 — 1 commitsTue 9:00 — 2 commitsTue 10:00 — 3 commitsTue 11:00 — 3 commitsTue 12:00 — 1 commitsTue 13:00 — 1 commitsTue 14:00 — 3 commitsTue 15:00 — 1 commitsTue 16:00 — 6 commitsTue 17:00 — 3 commitsTue 18:00 — 2 commitsTue 19:00 — 0 commitsTue 20:00 — 1 commitsTue 21:00 — 0 commitsTue 22:00 — 2 commitsTue 23:00 — 7 commitsWed 0:00 — 7 commitsWed 1:00 — 4 commitsWed 2:00 — 0 commitsWed 3:00 — 0 commitsWed 4:00 — 0 commitsWed 5:00 — 1 commitsWed 6:00 — 2 commitsWed 7:00 — 0 commitsWed 8:00 — 2 commitsWed 9:00 — 0 commitsWed 10:00 — 0 commitsWed 11:00 — 2 commitsWed 12:00 — 1 commitsWed 13:00 — 3 commitsWed 14:00 — 1 commitsWed 15:00 — 0 commitsWed 16:00 — 2 commitsWed 17:00 — 4 commitsWed 18:00 — 6 commitsWed 19:00 — 1 commitsWed 20:00 — 0 commitsWed 21:00 — 1 commitsWed 22:00 — 0 commitsWed 23:00 — 2 commitsThu 0:00 — 0 commitsThu 1:00 — 0 commitsThu 2:00 — 2 commitsThu 3:00 — 0 commitsThu 4:00 — 0 commitsThu 5:00 — 0 commitsThu 6:00 — 0 commitsThu 7:00 — 0 commitsThu 8:00 — 0 commitsThu 9:00 — 1 commitsThu 10:00 — 0 commitsThu 11:00 — 0 commitsThu 12:00 — 0 commitsThu 13:00 — 0 commitsThu 14:00 — 4 commitsThu 15:00 — 1 commitsThu 16:00 — 0 commitsThu 17:00 — 4 commitsThu 18:00 — 1 commitsThu 19:00 — 2 commitsThu 20:00 — 0 commitsThu 21:00 — 1 commitsThu 22:00 — 1 commitsThu 23:00 — 1 commitsFri 0:00 — 0 commitsFri 1:00 — 0 commitsFri 2:00 — 0 commitsFri 3:00 — 0 commitsFri 4:00 — 0 commitsFri 5:00 — 0 commitsFri 6:00 — 0 commitsFri 7:00 — 0 commitsFri 8:00 — 0 commitsFri 9:00 — 0 commitsFri 10:00 — 0 commitsFri 11:00 — 0 commitsFri 12:00 — 0 commitsFri 13:00 — 0 commitsFri 14:00 — 1 commitsFri 15:00 — 0 commitsFri 16:00 — 0 commitsFri 17:00 — 3 commitsFri 18:00 — 0 commitsFri 19:00 — 1 commitsFri 20:00 — 0 commitsFri 21:00 — 0 commitsFri 22:00 — 0 commitsFri 23:00 — 0 commitsSat 0:00 — 0 commitsSat 1:00 — 0 commitsSat 2:00 — 0 commitsSat 3:00 — 0 commitsSat 4:00 — 0 commitsSat 5:00 — 0 commitsSat 6:00 — 0 commitsSat 7:00 — 0 commitsSat 8:00 — 2 commitsSat 9:00 — 2 commitsSat 10:00 — 0 commitsSat 11:00 — 2 commitsSat 12:00 — 1 commitsSat 13:00 — 1 commitsSat 14:00 — 0 commitsSat 15:00 — 0 commitsSat 16:00 — 1 commitsSat 17:00 — 0 commitsSat 18:00 — 1 commitsSat 19:00 — 1 commitsSat 20:00 — 0 commitsSat 21:00 — 1 commitsSat 22:00 — 0 commitsSat 23:00 — 1 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.
DateListRankStars gained
Feb 20, 2026daily#22+112
  • freeCodeCamp/freeCodeCamp

    freeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.

    453.6K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    385.5K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • anomalyco/opencode

    The open source coding agent.

    194.7K stars · TypeScript