A macOS local development gateway. Say goodbye to localhost:port — each project gets its own domain, ready to use on first visit with automatic startup.
One solution covering local, LAN, and public access — .local domains let phones and nearby devices connect directly, and a single command generates a public URL via Tunnel. Works great with AI IDEs like Cursor and Windsurf.
Every project on localhost shares the same origin. This causes real problems:
- Cookies collide — session tokens, JWTs, and auth cookies from one project leak into another, causing mysterious login loops and 403 errors
- Saved passwords mix up — the browser autofills credentials from project A into project B because they're both
localhost - Browser history is useless —
localhost:3000,localhost:3001,localhost:8080... which project was which? - localStorage/IndexedDB overlap — data from different projects stomps on each other when they use the same keys
- Port conflicts — "address already in use" when two projects default to the same port; you waste time hunting which process to kill
- Remembering ports — was it
:3000or:3001? You end up grepping configs or checkinglsof - AI coding gets confused too — when you tell Cursor or Claude Code "restart my server", the AI has to figure out which port, which process, which command — and often gets it wrong
Coulson gives every project its own domain (myapp.coulson.local). Cookies, storage, passwords, and history are isolated by the browser automatically — the way they were designed to work. And when you tell your AI assistant "restart myapp", it just runs coulson restart myapp — no ports to remember, no processes to hunt down. The entire process group is killed cleanly (SIGTERM → SIGKILL), so child processes like file watchers, worker threads, and bundlers don't linger as orphans hogging ports.
- Zero-config routing — directory/file name becomes the domain (
myapp→myapp.coulson.local) - Auto-managed Python ASGI — starts on first request, stops after idle timeout
- Auto-managed Node.js — detects package manager and start script, starts on first request
- Auto-managed Docker Compose —
docker compose upon first request,downon idle - Static directory hosting — just drop a
publicdirectory - Multi-route — path-prefix routing to different backends under one domain
.coulson.toml— per-app configuration for routes, env, hooks, and proxy options- Lifecycle hooks — run scripts or fire webhooks on app start/stop/ready events
- mDNS —
.localdomains work out of the box, LAN and mobile devices connect directly - Cloudflare Tunnel — one command generates a public URL for sharing
- Web Dashboard + Menu bar app — visual management
Download Coulson.app and open it. The daemon starts automatically.
Click Install Command Line Tool... in the menu bar to use the coulson command in the terminal.
Generate a local CA certificate and add it to the system keychain for HTTPS support:
sudo coulson trustTake over ports 80/443 so you can omit port numbers when accessing:
sudo coulson trust --forwardListens on 127.0.0.1:18080 (HTTP) and 127.0.0.1:18443 (HTTPS) by default.
Map an existing service to a local domain:
echo 3000 > ~/.coulson/myappcurl -i http://myapp.coulson.local:18080/Example project structure:
~/Projects/hello/
app.py # async def app(scope, receive, send): ...
pyproject.toml
.venv/bin/uvicorn
Symlink to Coulson directory:
ln -s ~/Projects/hello ~/.coulson/hellocurl -i http://hello.coulson.local:18080/First request auto-starts uvicorn. Reaped after 15 minutes idle.
Example project structure:
~/Projects/myapi/
index.js # const http = require("http"); ...
package.json # scripts: { "dev": "bun run index.js" }
bun.lock
Symlink to Coulson directory:
ln -s ~/Projects/myapi ~/.coulson/myapicurl -i http://myapi.coulson.local:18080/First request auto-detects the package manager (bun/pnpm/yarn/npm), allocates a free port via the PORT environment variable, and runs the dev or start script. Reaped after 15 minutes idle.
Projects with a Procfile (or Procfile.dev) containing a web: process are auto-managed:
~/Projects/myapp/
Procfile # web: bundle exec rails server -p $PORT
ln -s ~/Projects/myapp ~/.coulson/myappcurl -i http://myapp.coulson.local:18080/First request allocates a free port via $PORT, runs the web command, and proxies traffic. Procfile.dev takes priority over Procfile when both exist.
To start companion processes (workers, etc.) alongside the web process, add to .coulsonrc:
COULSON_MANAGED_SERVICES=web,worker
All listed process types from the Procfile are started together and share the same lifecycle — idle timeout reaps the entire group.
Projects with a compose.yml (or docker-compose.yml) are auto-managed:
~/Projects/myapp/
compose.yml # services: web: ...
ln -s ~/Projects/myapp ~/.coulson/myappcurl -i http://myapp.coulson.local:18080/First request runs docker compose up -d --build. Idle containers are stopped via docker compose down after the idle timeout. Port discovery uses compose port mappings or $PORT env var.
Projects with a public subdirectory are automatically served as static files:
~/Projects/docs/
public/
index.html
style.css
ln -s ~/Projects/docs ~/.coulson/docscurl -i http://docs.coulson.local:18080/Changes are picked up automatically within 2 seconds.
Add a .coulsonrc file to any managed app directory to set environment variables:
# ~/Projects/myapp/.coulsonrc
PORT=4000
DATABASE_URL=postgres://localhost/myapp_dev
When PORT is set, the app always starts on that fixed port instead of auto-allocating one. Supports KEY=VALUE format with # comments, optional quoting, and export prefix.
For full control, add a .coulson.toml in the app directory:
name = "myapp"
domain = "myapp" # prefix only, suffix appended at runtime
kind = "asgi" # asgi, node, procfile, docker
# Process
module = "mymodule:app" # ASGI module
server = "uvicorn" # ASGI server
# Proxy options
port = 5006
timeout = 5000
cors = false
spa = false
# Remote env injection (fetched before each cold start)
env_url = "https://vault.example.com/env/myapp"
env_url_headers = { Authorization = "Bearer xxx" }
# Environment variables
[env]
DATABASE_URL = "postgres://localhost/myapp_dev"
# Multi-route
[[routes]]
path = "/api"
target = "127.0.0.1:3000"
timeout = 30000
# Lifecycle hooks
[hooks]
[hooks.app_ready]
run = "mise run db:migrate"
webhook = "https://hooks.slack.com/xxx"Coulson fires hooks on app lifecycle events. Global hooks are executable scripts in ~/.coulson/hooks/:
~/.coulson/hooks/
app_ready # runs when any app becomes ready
app_stop # runs when any app stops
scan_complete # runs after directory scan
Per-app hooks are configured via [hooks] in .coulson.toml (see above). Each hook receives COULSON_APP_NAME, COULSON_APP_URL, COULSON_APP_DOMAIN, and other context as environment variables.
Per-app events: app_add, app_remove, app_start, app_ready, app_stop, app_idle, tunnel_start, tunnel_stop. Global-only events: scan_complete.
Start/stop tunnels via CLI:
coulson tunnel start myapp
coulson tunnel stop myappAlso available via the Web Dashboard or the menu bar app.
No configuration needed — assigns a random *.trycloudflare.com URL, great for ad-hoc sharing. Requires cloudflared:
brew install cloudflaredConfigure wildcard DNS for your own domain (e.g. *.example.com) pointing to a Cloudflare Tunnel. Coulson automatically routes subdomains to local projects:
myapp.example.com→ localmyapphello.example.com→ localhello
All projects share one Tunnel connection — no per-app setup needed, new projects are instantly accessible from the public internet.
- Web Dashboard:
http://coulson.local:18080 - CLI:
coulson ls,coulson add,coulson restart,coulson open - Menu bar app: Coulson.app menu bar icon
Supports TOML config file (~/.config/coulson/config.toml) and environment variables. See example.
Priority: defaults < config file < environment variables.
- Rust + Pingora (reverse proxy)
- Swift (macOS menu bar app)
- Cloudflare Tunnel (public sharing)
This project is not affiliated with Cloudflare. It uses official Cloudflare APIs and respects all rate limits and account restrictions. Users are responsible for complying with Cloudflare's Terms of Service.
See LICENSE for details.
