The Problem

Local development often relies on hardcoded localhost:3000 or similar port addresses. When multiple developers run services simultaneously, port conflicts occur, and automated agents cannot reliably reference services by URL without knowing the dynamically assigned port. This creates friction in reproducibility and integration workflows.

What This Does

portless replaces port numbers with stable, named .localhost URLs. It functions as a local proxy that binds an available port (defaulting to 443 with HTTPS enabled) and maps the running application to a predictable hostname such as myapp.localhost.

The core logic resides in packages/portless/src/cli.ts, which parses arguments and orchestrates the start sequence. It invokes the proxy module at packages/portless/src/proxy.ts, which handles port allocation from the 4000–4999 range and manages the local Certificate Authority for TLS. Configuration is read from packages/portless/src/config.ts, which looks for a portless.json file at the project root or in the current directory. For monorepos, a single portless.json at the workspace root can configure all packages, discovered via pnpm-workspace.yaml or the workspaces field in package.json.

How It Is Wired

Execution begins at packages/portless/src/cli.ts. The CLI validates the environment; in non-interactive modes (no TTY or CI=1), it exits early with an error rather than prompting. If conditions are met, it reads the project configuration via packages/portless/src/config.ts.

The flow proceeds to packages/portless/src/service.ts, which manages the lifecycle of the proxy process. The proxy then intercepts the dev script defined in package.json. For frameworks that natively respect the PORT environment variable (Next.js, Express, FastAPI, Nuxt), the proxy sets this variable and starts the process. For frameworks that do not (Vite, Astro, Angular, Expo), the proxy analyzes the dev script in packages/portless/src/utils.ts and auto-injects the necessary --port and --host flags. The proxy auto-starts and reuses the most recent configuration (port, TLS settings, TLDs) on restart, ensuring consistency across reboots. Explicit environment variables like PORTLESS_PORT always take priority over inferred defaults.

How To Use It

Setup: Install globally or as a project dependency:

npm install -g portless
# or
npm install -D portless

Configuration: A portless.json file can override the default app name:

{ "name": "myapp" }

Running it: The primary entry point is the CLI script:

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

Running portless from a project root defaults to executing the "dev" script from package.json, producing a URL based on the package name.

Real-World Use

A team working with a monorepo can place one portless.json at the root. This allows any contributor to run portless from the repository root and start all workspace packages simultaneously, each receiving a stable *.localhost URL. An CI pipeline can invoke portless to spin up a development environment for integration tests without manually parsing port outputs or managing conflicting port assignments.

Code Health & Issues

Static analysis of the codebase identified several maintainability concerns:

  • Oversized files (High): Three files exceed 1100 lines and are difficult to hold in a single context: packages/portless/src/cli-utils.ts, packages/portless/src/certs.ts, and packages/portless/src/service.ts. Changes to these files have wide ripple effects due to their size.
  • Duplicated code blocks (High): 67 repeated 6-line blocks were found across 24 files, including apps/docs/src/components/docs-chat.tsx, apps/docs/src/components/search.tsx, and examples/google-oauth/src/app/page.tsx. The shared logic should be extracted into helpers.
  • Deep nesting (High): packages/portless/src/cli.ts and apps/docs/src/components/docs-chat.tsx exhibit a maximum indentation depth of 8, making control flow hard to follow. Guard clauses or extracted blocks are recommended.
  • Empty catch block (Medium): packages/portless/src/turbo.ts contains an empty catch {}, which silently discards errors.
  • Outdated dependencies: Several declared dependencies are behind the currently published major version. For example, typescript is declared ^5 but the current published version is 7.0.2, and @types/node is declared ^24.12.4 against a current 26.2.0.

The Bottom Line

portless effectively solves the problem of unstable local URLs by providing a proxy layer that maps apps to named .localhost domains. It handles framework-specific port injection adequately and offers monorepo support out of the box. However, the codebase contains several oversized files and duplicated logic that increase cognitive load for contributors. Additionally, dependency versions are lagging behind published releases. This tool is practical for teams needing consistent local URLs, but the internal structure would benefit from refactoring to reduce file size and duplicated complexity before a 1.0 release.