An open API service indexing awesome lists of open source software.

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.

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

![Request Inspector](https://coulson.hola.ac/assets/screenshot-inspector.png)

## 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.