https://github.com/ratazzi/coulson
Pow-inspired local development gateway. Drop a project, get a domain, start coding.
https://github.com/ratazzi/coulson
asgi cloudflare-tunnel dev-server development-tools dns local-development localhost macos portless pow procfile reverse-proxy rust
Last synced: 3 days ago
JSON representation
Pow-inspired local development gateway. Drop a project, get a domain, start coding.
- Host: GitHub
- URL: https://github.com/ratazzi/coulson
- Owner: ratazzi
- License: mit
- Created: 2026-02-07T09:15:08.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2026-04-03T16:17:43.000Z (5 days ago)
- Last Synced: 2026-04-03T17:43:26.842Z (5 days ago)
- Topics: asgi, cloudflare-tunnel, dev-server, development-tools, dns, local-development, localhost, macos, portless, pow, procfile, reverse-proxy, rust
- Language: Rust
- Homepage:
- Size: 1.48 MB
- Stars: 17
- Watchers: 1
- Forks: 2
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Coulson
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.
## Why Not Just `localhost:port`?
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 `:3000` or `:3001`? You end up grepping configs or checking `lsof`
- **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.
## Features
- **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 up` on first request, `down` on idle
- **Static directory hosting** — just drop a `public` directory
- **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** — `.local` domains 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

## Install
Download [Coulson.app](https://github.com/ratazzi/coulson/releases) and open it. The daemon starts automatically.
Click **Install Command Line Tool...** in the menu bar to use the `coulson` command in the terminal.
### Trust Certificate (optional)
Generate a local CA certificate and add it to the system keychain for HTTPS support:
```bash
sudo coulson trust
```
### Port Forwarding (optional)
Take over ports 80/443 so you can omit port numbers when accessing:
```bash
sudo coulson trust --forward
```
## Quick Start
Listens on `127.0.0.1:18080` (HTTP) and `127.0.0.1:18443` (HTTPS) by default.
### Port Proxy
Map an existing service to a local domain:
```bash
echo 3000 > ~/.coulson/myapp
```
```bash
curl -i http://myapp.coulson.local:18080/
```
### Python ASGI App
Example project structure:
```
~/Projects/hello/
app.py # async def app(scope, receive, send): ...
pyproject.toml
.venv/bin/uvicorn
```
Symlink to Coulson directory:
```bash
ln -s ~/Projects/hello ~/.coulson/hello
```
```bash
curl -i http://hello.coulson.local:18080/
```
First request auto-starts uvicorn. Reaped after 15 minutes idle.
### Node.js App
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:
```bash
ln -s ~/Projects/myapi ~/.coulson/myapi
```
```bash
curl -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.
### Procfile App
Projects with a `Procfile` (or `Procfile.dev`) containing a `web:` process are auto-managed:
```
~/Projects/myapp/
Procfile # web: bundle exec rails server -p $PORT
```
```bash
ln -s ~/Projects/myapp ~/.coulson/myapp
```
```bash
curl -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.
### Docker Compose App
Projects with a `compose.yml` (or `docker-compose.yml`) are auto-managed:
```
~/Projects/myapp/
compose.yml # services: web: ...
```
```bash
ln -s ~/Projects/myapp ~/.coulson/myapp
```
```bash
curl -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.
### Static Directory
Projects with a `public` subdirectory are automatically served as static files:
```
~/Projects/docs/
public/
index.html
style.css
```
```bash
ln -s ~/Projects/docs ~/.coulson/docs
```
```bash
curl -i http://docs.coulson.local:18080/
```
Changes are picked up automatically within 2 seconds.
### `.coulsonrc` — Per-App Environment
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.
### `.coulson.toml` — Per-App Configuration
For full control, add a `.coulson.toml` in the app directory:
```toml
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"
```
### Global Hooks
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`.
## Cloudflare Tunnel
Start/stop tunnels via CLI:
```bash
coulson tunnel start myapp
coulson tunnel stop myapp
```
Also available via the Web Dashboard or the menu bar app.
### Quick Tunnel
No configuration needed — assigns a random `*.trycloudflare.com` URL, great for ad-hoc sharing. Requires `cloudflared`:
```bash
brew install cloudflared
```
### Named Tunnel (recommended)
Configure 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` → local `myapp`
- `hello.example.com` → local `hello`
All projects share one Tunnel connection — no per-app setup needed, new projects are instantly accessible from the public internet.
## Management
- **Web Dashboard**: `http://coulson.local:18080`
- **CLI**: `coulson ls`, `coulson add`, `coulson restart`, `coulson open`
- **Menu bar app**: Coulson.app menu bar icon
## Configuration
Supports TOML config file (`~/.config/coulson/config.toml`) and environment variables. See [example](config.example.toml).
Priority: defaults < config file < environment variables.
## Built With
- [Rust](https://www.rust-lang.org/) + [Pingora](https://github.com/cloudflare/pingora) (reverse proxy)
- [Swift](https://www.swift.org/) (macOS menu bar app)
- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) (public sharing)
## Disclaimer
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](https://www.cloudflare.com/terms/).
## License
See [LICENSE](LICENSE) for details.