Skip to main content

Using Subdomains

Subdomains let you organize multiple related services under a common parent domain:
portless myapp next dev
# -> http://myapp.localhost:1355

portless api.myapp pnpm start
# -> http://api.myapp.localhost:1355

portless docs.myapp vite dev
# -> http://docs.myapp.localhost:1355
Each service gets its own URL, but they share a common namespace (myapp.localhost).

Wildcard Subdomain Routing

Portless automatically routes wildcard subdomains to the longest matching registered route.

How It Works

1
Register a route
2
portless myapp next dev
# Registers: myapp.localhost -> port 4123
3
Access any subdomain
4
# All of these route to the same app:
http://myapp.localhost:1355
http://tenant1.myapp.localhost:1355
http://tenant2.myapp.localhost:1355
http://anything.myapp.localhost:1355

Routing Priority

Portless matches routes with the following priority:
  1. Exact match: api.myapp.localhost matches api.myapp.localhost (if registered)
  2. Wildcard match: tenant1.api.myapp.localhost matches api.myapp.localhost
  3. Longer suffix match: If both myapp.localhost and api.myapp.localhost are registered, tenant1.api.myapp.localhost routes to api.myapp.localhost (longer suffix wins)
function findRoute(
  routes: { hostname: string; port: number }[],
  host: string
): { hostname: string; port: number } | undefined {
  return (
    // 1. Exact match first
    routes.find((r) => r.hostname === host) ||
    // 2. Wildcard match (longest suffix)
    routes.find((r) => host.endsWith("." + r.hostname))
  );
}

Multi-Tenant Applications

Wildcard routing is ideal for multi-tenant apps where each tenant gets their own subdomain:
portless myapp next dev
# -> http://myapp.localhost:1355

# Tenants access via their subdomain:
# http://acme.myapp.localhost:1355
# http://globex.myapp.localhost:1355
# http://initech.myapp.localhost:1355
Your app can extract the tenant ID from the Host header:
import { headers } from "next/headers";

export default function Page() {
  const host = headers().get("host") || "";
  const tenant = host.split(".")[0];
  
  return <div>Welcome, {tenant}!</div>;
}

Monorepo Service Organization

In a monorepo, use subdomains to namespace services:
{
  "name": "@monorepo/frontend",
  "scripts": {
    "dev": "portless frontend.myapp next dev"
  }
}
Now you have:
  • Frontend: http://frontend.myapp.localhost:1355
  • API: http://api.myapp.localhost:1355
  • Docs: http://docs.myapp.localhost:1355
You can also use portless run to infer the service name from package.json. If you set "name": "frontend", then portless run next dev will automatically use frontend.localhost as the hostname.

Cross-Service Communication

When one service needs to call another, use the portless URL:
// app/page.tsx
export default async function Page() {
  // Call the API service
  const res = await fetch("http://api.myapp.localhost:1355/users");
  const users = await res.json();
  
  return <UserList users={users} />;
}
When proxying between portless apps, always set changeOrigin: true. Without it, the proxy forwards the original Host header, causing portless to route the request back to the frontend in an infinite loop.Portless detects this and responds with 508 Loop Detected along with a helpful error message.

Environment-Specific URLs

Use the PORTLESS_URL environment variable to reference the current service’s URL:
// next.config.js
module.exports = {
  env: {
    NEXT_PUBLIC_API_URL: process.env.PORTLESS_URL + "/api",
  },
};

Combining Subdomains with Worktrees

Subdomains and worktree prefixes compose naturally:
# Main worktree (main branch)
cd ~/monorepo
portless frontend.myapp pnpm dev
# -> http://frontend.myapp.localhost:1355
portless api.myapp pnpm start
# -> http://api.myapp.localhost:1355

# Worktree (feature/auth branch)
cd ~/worktrees/monorepo-auth
portless frontend.myapp pnpm dev
# -> http://auth.frontend.myapp.localhost:1355
portless api.myapp pnpm start
# -> http://auth.api.myapp.localhost:1355
The worktree prefix (auth) is prepended to the full service name.

DNS Label Length Limit

DNS labels (the parts between dots) are limited to 63 characters per RFC 1035. Portless automatically truncates long labels and appends a hash suffix for uniqueness:
export function truncateLabel(label: string): string {
  if (label.length <= MAX_DNS_LABEL_LENGTH) return label;
  
  // 6-char hex hash from the full label for uniqueness
  const hash = createHash("sha256").update(label).digest("hex").slice(0, 6);
  
  // Reserve space for "-" separator + 6-char hash = 7 chars
  const maxPrefixLength = MAX_DNS_LABEL_LENGTH - 7;
  const prefix = label.slice(0, maxPrefixLength).replace(/-+$/, "");
  
  return `${prefix}-${hash}`;
}
Example:
portless this-is-a-very-long-service-name-that-exceeds-the-dns-label-limit next dev
# -> http://this-is-a-very-long-service-name-that-exceeds-the-dns-la-a1b2c3.localhost:1355

Static Aliases for Non-Portless Services

Use portless alias to register routes for services not managed by portless (e.g., Docker containers, databases):
# Register a PostgreSQL container
portless alias postgres 5432
# -> http://postgres.localhost:1355 routes to localhost:5432

# Register a Redis container
portless alias redis 6379
# -> http://redis.localhost:1355 routes to localhost:6379

# Remove an alias
portless alias --remove postgres
Aliases show up in portless list with (alias) instead of a PID:
portless list
# Active routes:
#   http://myapp.localhost:1355  ->  localhost:4123  (pid 12345)
#   http://postgres.localhost:1355  ->  localhost:5432  (alias)

Subdomain Naming Best Practices

Use Short, Descriptive Names

portless api.myapp pnpm start
portless docs.myapp vite dev
portless admin.myapp next dev
portless frontend.myapp next dev
portless api.myapp pnpm start
portless admin.myapp next dev

Use Consistent Naming

portless frontend.myapp next dev
portless backend.myapp pnpm start

Wildcard Routing Edge Cases

Deep Nesting

Wildcard routing works at any depth:
portless myapp next dev
# Registers: myapp.localhost

# All of these route to the same app:
http://a.myapp.localhost:1355
http://a.b.myapp.localhost:1355
http://a.b.c.myapp.localhost:1355

Overlapping Routes

If you register multiple overlapping routes, the longest suffix wins:
portless myapp next dev          # port 4123
portless api.myapp pnpm start    # port 4567

# Routing:
http://myapp.localhost:1355           -> 4123 (exact match)
http://api.myapp.localhost:1355       -> 4567 (exact match)
http://tenant.myapp.localhost:1355    -> 4123 (wildcard: *.myapp.localhost)
http://tenant.api.myapp.localhost:1355 -> 4567 (wildcard: *.api.myapp.localhost)

Build docs developers (and LLMs) love