Skip to main content

Overview

Portless automatically routes wildcard subdomains to their parent route. Once you register an app (e.g., myapp), any subdomain of that app routes to it automatically without extra configuration. This is especially useful for:
  • Multi-tenant applications
  • Dynamic preview environments
  • Per-user subdomains
  • Testing subdomain-based features locally

How It Works

When portless receives a request, it:
  1. Checks for exact hostname match - If myapp.localhost is registered, it matches first
  2. Falls back to wildcard matching - If no exact match, checks if the hostname ends with . + a registered route
  3. Routes to the parent - tenant1.myapp.localhost routes to the app registered at myapp.localhost
You only register the base name once. All subdomains work automatically.

Quick Example

No extra registration needed. The app sees the full hostname in the Host header and can route internally.

Multi-Tenant Applications

Setup

1

Register your app once

2

Access different tenants

Each tenant gets a unique subdomain:
3

Handle routing in your app

Your app reads the Host header to determine the tenant:

Express Example

Next.js Example

middleware.ts

Dynamic Preview Environments

Wildcard routing is perfect for branch-based or PR-based preview URLs:
Combine with git worktrees for automatic subdomain prefixes based on branch names.

Per-User Subdomains

Build GitHub-style user pages or per-user dashboards:
Your app extracts the username from the subdomain and serves personalized content.

Real-World Examples

SaaS Application with Tenant Isolation

Localization Testing

Microservices with Per-Service Subdomains

Exact matches take precedence over wildcards. If you register both myapp and api.myapp, requests to api.myapp.localhost route to the explicit api.myapp registration, not the wildcard for myapp.

How Wildcard Matching Works

Matching Algorithm

Priority Order

  1. Exact hostname match - myapp.localhost matches myapp registration
  2. Longest wildcard match - api.myapp.localhost matches api.myapp before myapp
  3. 404 - No match found
Wildcard matching uses endsWith("." + hostname), so subdomains must include the full parent name. tenant.myapp.localhost matches myapp.localhost, but tenantmyapp.localhost does not.

Headers and Request Context

X-Forwarded-* Headers

Portless adds standard forwarding headers to every request:
Your app can use these to reconstruct the original request URL:

Host Header

The Host header is not rewritten - your app receives the full subdomain:
This preserves the original request context and allows your app to route based on the subdomain.

Combining with Static Routes

You can mix wildcard routing with static routes for external services:
Static routes take precedence over wildcards because they create exact hostname matches.

Limitations

Safari and .localhost DNS

Safari relies on system DNS resolution, which may not auto-resolve .localhost subdomains. If you see DNS errors:
This adds all active routes to /etc/hosts. See Safari DNS for details.

HTTPS and Wildcards

When using HTTPS, portless generates per-hostname certificates on-demand via SNI (Server Name Indication). The first request to a new subdomain triggers certificate generation, which takes ~100ms. Subsequent requests to the same subdomain use the cached certificate. See HTTPS & HTTP/2 for details.

No Deep Wildcard Nesting

Wildcard matching is simple suffix-based. If you register myapp, these work:
  • sub.myapp.localhost
  • deep.sub.myapp.localhost
  • any.depth.works.myapp.localhost
But portless doesn’t distinguish between levels - they all route to the same app at myapp.

Debugging

Check Active Routes

Outputs:

Test with curl

Proxy Logs

Run the proxy in foreground mode to see routing decisions:
Requests are logged with the matched route and target port.

Use Cases Summary

Wildcard routing means you register once and forget. Your app handles the subdomain logic, and portless handles the routing.