Skip to main content
OrchestKit v7.30.1 — 103 skills, 36 agents, 169 hooks · Claude Code 2.1.76+
OrchestKit
Skills

Portless

Named .localhost URLs for local development with portless. Eliminates port collisions, enables stable URLs for agents, integrates with emulate for API emulation aliases and git worktrees for branch-named subdomains. Use when setting up local dev environments, configuring agent-accessible URLs, or running multi-service dev setups. Do NOT use for production deployments, CI environments (set PORTLESS=0), or DNS/hosting configuration.

Reference low

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

Portless Integration

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

Full CLI reference: Load Read("$\{CLAUDE_SKILL_DIR\}/references/upstream.md") for complete command docs.

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

# Instead of: npm run dev (random port)
portless run npm run dev
# → https://myapp.localhost (stable, named, HTTPS)

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

Framework-Specific Setup

Load Read("$\{CLAUDE_SKILL_DIR\}/references/framework-integration.md") for full framework recipes.

Most frameworks (Next.js, Vite, Express) work with portless run <cmd>. Some need explicit flags:

FrameworkAuto-detected?Extra flags needed
Next.jsYesNone
Vite / AstroYesNone
Express / Fastify / HonoYesNone (reads PORT env var)
Ruby on RailsYesNone
FastAPI / uvicornNo--port $PORT --host $HOST
DjangoNo$HOST:$PORT positional arg

Why .localhost?

Feature.localhost (RFC 6761)127.0.0.1:PORT/etc/hosts hack
No /etc/hosts editingYesYesNo
HTTPS with valid certYesNoManual
Wildcard subdomainsYesNoNo
Works in all browsersYesYesVaries
Cookie isolation per serviceYesNoYes
No port conflictsYesNoYes

Key Environment Variables

When portless runs your command, it injects:

VariableValueUse in agents
PORTAssigned ephemeral port (4000-4999)Internal only
HOST127.0.0.1Internal only
PORTLESS_URLhttps://myapp.localhostUse this in agent prompts

OrchestKit Integration Patterns

1. Agent-Accessible Dev Server

# 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)

# 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

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

4. Bypass in CI

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

Anti-Patterns

Don'tDo Instead
Hardcode localhost:3000 in testsUse PORTLESS_URL or process.env.PORTLESS_URL
Run portless in CISet PORTLESS=0 in CI environments
Use numeric ports in AGENTS.mdDocument the portless URL

References

FileContent
references/upstream.mdFull portless CLI reference (synced from Vercel)
references/upstream-oauth.mdOAuth callback patterns with stable URLs
references/framework-integration.mdFramework recipes (FastAPI, Django, Docker, gotchas)
checklists/new-project-setup.mdStep-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:

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:

// 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:

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

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

// 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:

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

Proxy config for API backends:

// 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:

# 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:

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:

# 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:

portless run --name app rails server

Go (net/http)

Read from environment in your code:

port := os.Getenv("PORT")
if port == "" {
    port = "8080"
}
http.ListenAndServe(":"+port, handler)
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.):

# 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

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).

// 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.localhostapi.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:

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

Replace with PORTLESS_URL or the named *.localhost URL.

Upstream Oauth

<!-- SYNCED from vercel-labs/portless (skills/oauth/SKILL.md) --> <!-- Hash: bbdfd32c800dd69a68348ac868e355bda886b8cdb57b587acd7a70327ceb9fc4 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

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:

Providerlocalhost.localhost subdomainsReason
GoogleAllowedRejectedNot in their bundled PSL
AppleRejectedRejectedNo localhost at all
MicrosoftAllowedAllowedPermissive localhost handling
FacebookAllowedVariesMust register each URI exactly
GitHubAllowedAllowedPermissive

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:

sudo portless proxy start --https -p 443 --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 subdomain of a domain you control:

sudo portless proxy start --https -p 443 --tld dev
portless myapp.local.yourcompany 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.

Provider Setup

Google

  1. Go to Google Cloud Console > 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
  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
  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
  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
  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:

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:

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:

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 -- browsers force HTTPS. Start the proxy with --https:

sudo portless proxy start --https -p 443 --tld dev

Port 443 avoids needing a port number in URLs. Run sudo 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 for a complete working example with Next.js + NextAuth + Google OAuth using --tld dev.

Upstream

<!-- SYNCED from vercel-labs/portless (skills/portless/SKILL.md) --> <!-- Hash: 542450881544362ac60a2dbbfe758207b29a9b4e98365702dcbaf9fcdb08f175 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

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

portless is a global CLI tool. Do NOT add it as a project dependency (no npm install portless or pnpm add portless in a project). Do NOT use npx or pnpm dlx.

Install globally:

npm install -g portless

Quick Start

# Install globally
npm install -g portless

# Start the proxy (once, no sudo needed)
portless proxy start

# Run your app (auto-starts the proxy if needed)
portless run next dev
# -> http://<project>.localhost:1355

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

The proxy auto-starts when you run an app. You can also start it explicitly with portless proxy start.

Integration Patterns

package.json scripts

{
  "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

portless myapp next dev          # http://myapp.localhost:1355
portless api.myapp pnpm start    # http://api.myapp.localhost:1355
portless docs.myapp next dev     # http://docs.myapp.localhost:1355

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:1355 routes to the myapp app without extra registration). 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:

# Main worktree -- no prefix
portless run next dev   # -> http://myapp.localhost:1355

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

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:

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

How It Works

  1. portless proxy start starts an HTTP reverse proxy on port 1355 as a background daemon (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 http://&lt;name&gt;.localhost:1355 on the proxy port; the proxy forwards to the app's assigned port

.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 sudo portless hosts sync to add entries to /etc/hosts if needed.

Most frameworks (Next.js, Express, Nuxt, etc.) respect the PORT env var automatically. For frameworks that ignore PORT (Vite, Astro, React Router, Angular, Expo, React Native), portless auto-injects the correct --port and --host CLI flags.

State directory

Portless stores its state (routes, PID file, port file) in a directory that depends on the proxy port:

  • Port < 1024 (sudo required): /tmp/portless (macOS/Linux only)
  • Port >= 1024 (no sudo): ~/.portless
  • Windows: Always ~/.portless (no privileged port concept)

Override with the PORTLESS_STATE_DIR environment variable.

Environment variables

VariableDescription
PORTLESS_PORTOverride the default proxy port (default: 1355)
PORTLESS_APP_PORTUse a fixed port for the app (skip auto-assignment)
PORTLESS_HTTPSSet to 1 to always enable HTTPS/HTTP/2
PORTLESS_TLDUse a custom TLD instead of localhost (e.g. test)
PORTLESS_WILDCARDSet to 1 to allow unregistered subdomains to fall back to parent
PORTLESS_SYNC_HOSTSSet to 1 to auto-sync /etc/hosts (auto-enabled for custom TLDs)
PORTLESS_STATE_DIROverride the state directory
PORTLESS=0Bypass the proxy, run the command directly

HTTP/2 + HTTPS

Use --https for HTTP/2 multiplexing (faster page loads for dev servers with many files):

portless proxy start --https                  # Auto-generate certs and trust CA
portless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs
sudo portless trust                           # Add CA to trust store later

First run generates a local CA and prompts for sudo to add it to the system trust store. After that, no prompts and no browser warnings. Set PORTLESS_HTTPS=1 in .bashrc/.zshrc to make it permanent.

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.

CLI Reference

CommandDescription
portless run &lt;cmd&gt; [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 http://&lt;name&gt;.localhost:1355 (auto-starts proxy)
portless get &lt;name&gt;Print URL for a service (for cross-service wiring)
portless get &lt;name&gt; --no-worktreePrint URL without worktree prefix
portless listShow active routes
portless trustAdd local CA to system trust store (for HTTPS)
portless proxy startStart the proxy as a daemon (port 1355, no sudo)
portless proxy start --httpsStart with HTTP/2 + TLS (auto-generates certs)
portless proxy start -p &lt;number&gt;Start the proxy on a custom port
portless proxy start --tld testUse .test instead of .localhost (requires /etc/hosts sync)
portless proxy start --foregroundStart the proxy in foreground (for debugging)
portless proxy start --wildcardAllow unregistered subdomains to fall back to parent route
portless proxy stopStop the proxy
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; --forceOverwrite an existing route
portless alias --remove &lt;name&gt;Remove a static route
portless hosts syncAdd routes to /etc/hosts (fixes Safari)
portless hosts cleanRemove 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; --force &lt;cmd&gt;Override an existing route registered by another process
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 / -hShow help
portless run --helpShow help for a subcommand (also: alias, hosts)
portless --version / -vShow version

Reserved names: run, get, alias, hosts, list, trust, and proxy 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.

Troubleshooting

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:

portless proxy start

Port already in use

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

portless proxy start -p 8080

Framework not respecting PORT

Portless auto-injects --port and --host flags for frameworks that ignore the PORT env var: Vite, Astro, React Router, Angular, Expo, and React Native. SvelteKit uses Vite internally and is handled automatically.

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

Ports below 1024 require sudo. The default port (1355) does not need sudo. If you want to use port 80:

sudo portless proxy start -p 80       # Port 80, requires sudo
portless proxy start                   # Port 1355, no sudo needed
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:

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

Auto-syncs /etc/hosts for custom TLDs (e.g. --tld test). For .localhost, set PORTLESS_SYNC_HOSTS=1 to enable.

Browser shows certificate warning with --https

The local CA may not be trusted yet. Run:

sudo portless trust

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

Proxy loop (508 Loop Detected)

If your dev server proxies requests to another portless app (e.g. Vite proxying /api to api.myapp.localhost:1355), 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.):

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

Requirements

  • Node.js 20+
  • macOS, Linux, or Windows
  • openssl (for --https cert generation; ships with macOS and most Linux distributions)

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:

PatternExampleWhen to use
&lt;project&gt;myappSingle-service project
&lt;project&gt;-&lt;layer&gt;myapp-api, myapp-webMulti-service / monorepo
&lt;service&gt;api, web, docsWhen 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:

// 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:

# 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:

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):

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

Vite (vite.config.ts):

server: {
  proxy: {
    "/api": { target: "https://api.localhost", changeOrigin: true, secure: false }
  }
}

5. CI Compatibility

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

# 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:

// 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

# 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.

Edit on GitHub

Last updated on

On this page

Portless IntegrationWhen to UseQuick StartFramework-Specific SetupWhy .localhost?Key Environment VariablesOrchestKit Integration Patterns1. Agent-Accessible Dev Server2. Emulate + Portless (Named API Mocks)3. Git Worktree Dev4. Bypass in CIAnti-PatternsReferencesReferences (3)Framework IntegrationFramework Integration RecipesHow Portless WorksFramework ReferenceNode.js (Express, Fastify, Hono)Next.jsVite / AstroFastAPI / UvicornDjangoRuby on RailsGo (net/http)Docker Infrastructure via AliasesCommon GotchasCookie DomainProxy 508 LoopMixed Port ReferencesUpstream OauthOAuth with PortlessThe ProblemThe FixUse a domain you ownProvider SetupGoogleAppleMicrosoft (Entra / Azure AD)Facebook (Meta)GitHubAuth Library ConfigurationNextAuth / Auth.jsPassport.jsGeneric / ManualTroubleshooting"redirect_uri_mismatch" or "invalid redirect URI"Provider requires HTTPSApple rejects the domainCallback goes to wrong URL after sign-inExampleUpstreamPortlessWhy portlessInstallationQuick StartIntegration Patternspackage.json scriptsMulti-app setups with subdomainsGit worktreesBypassing portlessHow It WorksState directoryEnvironment variablesHTTP/2 + HTTPSCLI ReferenceTroubleshootingProxy not runningPort already in useFramework not respecting PORTPermission errorsSafari can't find .localhost URLsBrowser shows certificate warning with --httpsProxy loop (508 Loop Detected)RequirementsChecklists (1)New Project SetupNew Project Setup ChecklistPrerequisitesSteps1. Choose Name(s)2. Update Dev Scripts3. Update Environment Variables4. Update Proxy Configs5. CI Compatibility6. Monorepo Pattern7. VerifyRollback