Skip to content

Repository files navigation

Repository social preview

Lewp

Lewp is a macOS-first local domain router/proxy for parallel local development. It leases stable loopback ports, assigns predictable .lewp hostnames, and reverse-proxies browser traffic to developer-started processes.

Lewp is not a process manager. It never starts Rails, Vite, Next, Docker, or any other app process.

Install

Release install:

curl -fsSL https://raw.githubusercontent.com/scottwater/lewp/main/install.sh | bash

Or with Go:

go install github.com/scottwater/lewp/cmd/lewp@latest

Or from a local checkout:

git clone <repo-url> lewp
cd lewp
bin/reinstall

This installs to ~/.local/bin/lewp by default. Make sure ~/.local/bin is in your PATH. Override with LEWP_INSTALL_DIR=/some/bin.

Requirements:

  • macOS
  • Go 1.25+
  • permission to install a LaunchAgent and trust a local development CA

Setup

Run one-time setup:

lewp setup
lewp system start

setup writes /etc/resolver/lewp, creates Lewp's local CA files under ~/Library/Application Support/lewp/, writes the LaunchAgent plist, and trusts the CA with macOS security.

Expect two prompts during setup:

  • sudo: installing /etc/resolver/lewp needs root, so setup runs sudo and macOS may prompt for your password.
  • keychain trust: trusting the local CA may pop a macOS dialog asking you to allow the change to your keychain.

If a step fails, setup prints the exact failing command and a Next: line with how to recover (for example, the precise security add-trusted-cert command to run by hand). system start runs the launchd bootstrap command; if the LaunchAgent is already loaded, it falls back to launchctl kickstart -k. Daemon logs are written under ~/Library/Logs/lewp/.

Check the installed build with:

lewp version

Upgrade release installs with:

lewp upgrade
lewp system restart

Release Versioning

Release versions come from git tags. There is no checked-in version file to update before a release; internal/buildinfo only contains fallback values for unstamped local builds.

To publish a release from the current committed state:

go test ./...
git tag v0.1.0
git push origin v0.1.0

Pushing a v* tag runs the GitHub release workflow. GoReleaser builds the macOS artifacts and injects the tag version into the binary, so release artifacts from v0.1.0 report lewp version 0.1.0. New installs and lewp upgrade use the latest GitHub release.

Quick Start

From a project instance directory:

cd ~/projects/atlas/feature-1
lewp lease

Example output:

PORT=42137
URL=http://feature-1.atlas.lewp
HTTPS_URL=https://feature-1.atlas.lewp
HOST=feature-1.atlas.lewp
STATE=new
HOST_KIND=instance

lease always prints both the HTTP URL and the HTTPS_URL whenever the lease has a host. Whether the browser accepts the HTTPS one depends on lewp setup having trusted the local CA (see HTTPS): .lewp hosts and configured safe-subtree custom suffixes both get browser-trusted certificates; domain-mirror suffixes stay HTTP-only. STATE and HOST_KIND are descriptive; use --shell for clean export lines (which omit them) when you want eval "$(lewp lease --shell)".

Start your app yourself on the leased port:

PORT=42137 <your dev command>

Then open either scheme:

http://feature-1.atlas.lewp
https://feature-1.atlas.lewp

Add extra hostnames for the same app process with alias. Aliases attach to the current route and reuse its port; they do not create another route or start another process:

lewp alias add tags.feature-1.atlas.lewp
lewp alias add '*.feature-1.atlas.lewp'
lewp alias list

Wildcards match one label only. *.feature-1.atlas.lewp matches tags.feature-1.atlas.lewp, but not feature-1.atlas.lewp or api.tags.feature-1.atlas.lewp.

Check or relocate a route later:

lewp info                                  # table of this directory's route and ports
PORT="$(lewp info --host feature-1.atlas.lewp --port)"
lewp move --from ~/projects/atlas/feature-1   # bring its route, aliases, and port here

Identity and configuration

A route's hostname is <name>.<root>.lewp. Lewp resolves root, name, and an optional explicit host from the first source that provides each, in priority order:

  1. Flagslewp lease --root atlas --name feature-1 --host atlas.lewp
  2. EnvironmentLEWP_ROOT, LEWP_NAME, LEWP_HOST
  3. Config file — the nearest .lewp.local.toml up the directory tree
  4. Inference — parent directory → root, current directory → name

The environment variables are read from the CLI process and forwarded to the daemon (the daemon never reads its own environment), so they work per-shell:

LEWP_ROOT=atlas LEWP_NAME=feature-1 lewp lease
export LEWP_HOST=sso.atlas.lewp     # pin an explicit host for this shell

Passing --root, --name, or --host to lewp lease is remembered for that directory. Use lewp lease --reset to clear those remembered route overrides and re-resolve from flags, environment, config, and inference while keeping the same port, aliases, and route history. Combine it with a flag to keep one value:

lewp lease --reset --root atlas

For a stable, path-independent identity, write a .lewp.local.toml with lewp init:

lewp init --root atlas --name feature-1
# writes ./.lewp.local.toml and prints a .git/info/exclude line to keep it local
root = "atlas"
name = "feature-1"
host = "atlas.lewp"   # optional explicit host

lewp init only writes the file; it never contacts the daemon or leases anything. The file is meant to stay uncommitted — init prints the .git/info/exclude line that keeps it out of version control. Re-run with --force to overwrite an existing file (rebuilt from flags and inference, never from the file being replaced).

Custom public dev suffixes

.lewp remains the default. For OAuth providers that reject private TLDs, use an owned public-domain subtree:

lewp setup --suffix local.todoordie.com
lewp lease --host feature-1.local.todoordie.com

Lewp installs a resolver only for the configured subtree. With local.todoordie.com, todoordie.com and www.todoordie.com continue to use normal DNS. This is the default safe-subtree mode. Custom suffixes must be below a registrable domain; apex domains such as todoordie.com and todoordie.co.uk are rejected unless you explicitly choose domain mirror mode.

Domain mirror mode is for local mirrors of an owned public domain:

lewp setup --suffix localkickofflabs.com --allow-domain-mirror
lewp lease --host localkickofflabs.com
lewp alias add app.localkickofflabs.com
lewp alias add leads.localkickofflabs.com

While the resolver file exists, that suffix shadows public DNS on this Mac. Proxy routing is still host-based: create the primary route once with lewp lease, then attach any same-app hostnames with lewp alias add. After adding a suffix, run lewp system start (or lewp setup --suffix localkickofflabs.com --allow-domain-mirror --start) so the daemon kickstarts and loads the updated suffix list.

Safe-subtree custom suffixes support HTTPS through Lewp's local CA, the same as .lewp hosts, once lewp setup has run for that suffix. Domain-mirror suffixes stay HTTP-only — Lewp's CA is cryptographically unable to sign a real registrable domain.

Route aliases

One app process can serve multiple local hostnames through the same Lewp route. Create the route once, start the app on that port, then add aliases:

lewp lease
lewp alias add tags.app.lewp
lewp alias add leads.app.lewp
lewp alias add '*.app.lewp'

Aliases reuse the current directory's active route and port. They never allocate a second app port. Wildcards match exactly one label: *.app.lewp matches tags.app.lewp, but not app.lewp or foo.tags.app.lewp.

For public-domain aliases, first configure the managed suffix with lewp setup --suffix ...; use --allow-domain-mirror only when you intentionally want to shadow that domain locally.

Worktree walkthrough

Lewp shines with git worktree, where each checkout needs its own stable URL and ports. Suppose your worktrees live somewhere non-standard — not under a tidy atlas/<name> layout:

cd ~/scratch/wt/atlas-login-fix          # a worktree checkout
lewp lease
# inferred root=wt name=atlas-login-fix      (note on stderr)
# PORT=42150
# URL=http://atlas-login-fix.wt.lewp
# HTTPS_URL=https://atlas-login-fix.wt.lewp
# HOST=atlas-login-fix.wt.lewp
# STATE=new
# HOST_KIND=instance

If the inferred root/name are not what you want, pin them explicitly so the host is predictable regardless of where the worktree lives:

lewp init --root atlas --name login-fix     # write .lewp.local.toml
lewp release --forget                        # drop the inferred wt.lewp route
lewp lease                                    # now -> login-fix.atlas.lewp

If two worktrees infer the same host, the second is given a deterministic suffix instead of stealing the first owner; pin distinct names (above) to avoid the suffix. Start your app on the leased port, then inspect and, after moving the worktree, relocate the route so its URL follows it:

PORT=42150 <your dev command>                 # Lewp routes; it never starts apps
lewp info                                      # this directory's route and ports

git worktree move ~/scratch/wt/atlas-login-fix ~/projects/atlas/login-fix
cd ~/projects/atlas/login-fix
lewp move --from ~/scratch/wt/atlas-login-fix  # same host, aliases, and port

HTTPS

lewp setup generates a local development CA and trusts it in the macOS login keychain, so every .lewp host is reachable over https:// with a certificate the browser accepts. Lewp mints per-host leaf certificates on demand from that CA; there is no per-project TLS config.

Safe-subtree custom suffixes (for example local.mycompany.com) get the same treatment: Lewp mints browser-trusted certificates for hosts under them, which satisfies OAuth providers that require a real TLD. Domain-mirror suffixes stay HTTP-only — Lewp's CA is cryptographically unable to sign a real registrable domain.

The CA certificate carries critical X.509 name constraints limited to .lewp plus your configured safe-subtree suffixes. Even if the CA key is stolen, a forged certificate for any other domain is rejected by the browser. Adding or removing a safe-subtree suffix rotates the CA on the next lewp setup (one keychain prompt); restart the daemon afterwards so HTTPS re-mints leaves.

Browser support in V1:

  • Supported: Safari and every Chromium-based browser (Chrome, Brave, Arc, Edge, Helium). These trust the macOS system keychain, so they pick up the CA automatically once lewp setup runs.
  • Not supported: Firefox. Firefox ships its own NSS trust store and ignores the macOS keychain, so it will warn on .lewp HTTPS in V1. Use Safari or a Chromium browser for HTTPS, or stay on http:// in Firefox. NSS/certutil trust is planned for a later release.

Run lewp doctor to confirm the CA is present and trusted.

CLI Reference

Full command documentation lives in DOCUMENTATION.md.

Development

go test ./...
bin/build

About

Lew is a macOS local domain router/proxy

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages