{"id":45813054,"url":"https://github.com/ratazzi/coulson","last_synced_at":"2026-04-05T18:02:07.507Z","repository":{"id":340802291,"uuid":"1152041204","full_name":"ratazzi/coulson","owner":"ratazzi","description":"Pow-inspired local development gateway. Drop a project, get a domain, start coding.","archived":false,"fork":false,"pushed_at":"2026-04-03T16:17:43.000Z","size":1554,"stargazers_count":17,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-04-03T17:43:26.842Z","etag":null,"topics":["asgi","cloudflare-tunnel","dev-server","development-tools","dns","local-development","localhost","macos","portless","pow","procfile","reverse-proxy","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ratazzi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-07T09:15:08.000Z","updated_at":"2026-04-03T13:00:48.000Z","dependencies_parsed_at":"2026-04-03T15:05:06.344Z","dependency_job_id":null,"html_url":"https://github.com/ratazzi/coulson","commit_stats":null,"previous_names":["ratazzi/coulson"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/ratazzi/coulson","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ratazzi%2Fcoulson","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ratazzi%2Fcoulson/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ratazzi%2Fcoulson/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ratazzi%2Fcoulson/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ratazzi","download_url":"https://codeload.github.com/ratazzi/coulson/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ratazzi%2Fcoulson/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31368156,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T17:53:18.093Z","status":"ssl_error","status_checked_at":"2026-04-03T17:53:17.617Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asgi","cloudflare-tunnel","dev-server","development-tools","dns","local-development","localhost","macos","portless","pow","procfile","reverse-proxy","rust"],"created_at":"2026-02-26T17:13:37.697Z","updated_at":"2026-04-03T18:01:32.245Z","avatar_url":"https://github.com/ratazzi.png","language":"Rust","readme":"# Coulson\n\nA macOS local development gateway. Say goodbye to `localhost:port` — each project gets its own domain, ready to use on first visit with automatic startup.\n\nOne 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.\n\n## Why Not Just `localhost:port`?\n\nEvery project on `localhost` shares the same origin. This causes real problems:\n\n- **Cookies collide** — session tokens, JWTs, and auth cookies from one project leak into another, causing mysterious login loops and 403 errors\n- **Saved passwords mix up** — the browser autofills credentials from project A into project B because they're both `localhost`\n- **Browser history is useless** — `localhost:3000`, `localhost:3001`, `localhost:8080`... which project was which?\n- **localStorage/IndexedDB overlap** — data from different projects stomps on each other when they use the same keys\n- **Port conflicts** — \"address already in use\" when two projects default to the same port; you waste time hunting which process to kill\n- **Remembering ports** — was it `:3000` or `:3001`? You end up grepping configs or checking `lsof`\n- **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\n\nCoulson 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.\n\n## Features\n\n- **Zero-config routing** — directory/file name becomes the domain (`myapp` → `myapp.coulson.local`)\n- **Auto-managed Python ASGI** — starts on first request, stops after idle timeout\n- **Auto-managed Node.js** — detects package manager and start script, starts on first request\n- **Auto-managed Docker Compose** — `docker compose up` on first request, `down` on idle\n- **Static directory hosting** — just drop a `public` directory\n- **Multi-route** — path-prefix routing to different backends under one domain\n- **`.coulson.toml`** — per-app configuration for routes, env, hooks, and proxy options\n- **Lifecycle hooks** — run scripts or fire webhooks on app start/stop/ready events\n- **mDNS** — `.local` domains work out of the box, LAN and mobile devices connect directly\n- **Cloudflare Tunnel** — one command generates a public URL for sharing\n- **Web Dashboard + Menu bar app** — visual management\n\n![Request Inspector](https://coulson.hola.ac/assets/screenshot-inspector.png)\n\n## Install\n\nDownload [Coulson.app](https://github.com/ratazzi/coulson/releases) and open it. The daemon starts automatically.\n\nClick **Install Command Line Tool...** in the menu bar to use the `coulson` command in the terminal.\n\n### Trust Certificate (optional)\n\nGenerate a local CA certificate and add it to the system keychain for HTTPS support:\n\n```bash\nsudo coulson trust\n```\n\n### Port Forwarding (optional)\n\nTake over ports 80/443 so you can omit port numbers when accessing:\n\n```bash\nsudo coulson trust --forward\n```\n\n## Quick Start\n\nListens on `127.0.0.1:18080` (HTTP) and `127.0.0.1:18443` (HTTPS) by default.\n\n### Port Proxy\n\nMap an existing service to a local domain:\n\n```bash\necho 3000 \u003e ~/.coulson/myapp\n```\n\n```bash\ncurl -i http://myapp.coulson.local:18080/\n```\n\n### Python ASGI App\n\nExample project structure:\n\n```\n~/Projects/hello/\n  app.py              # async def app(scope, receive, send): ...\n  pyproject.toml\n  .venv/bin/uvicorn\n```\n\nSymlink to Coulson directory:\n\n```bash\nln -s ~/Projects/hello ~/.coulson/hello\n```\n\n```bash\ncurl -i http://hello.coulson.local:18080/\n```\n\nFirst request auto-starts uvicorn. Reaped after 15 minutes idle.\n\n### Node.js App\n\nExample project structure:\n\n```\n~/Projects/myapi/\n  index.js            # const http = require(\"http\"); ...\n  package.json        # scripts: { \"dev\": \"bun run index.js\" }\n  bun.lock\n```\n\nSymlink to Coulson directory:\n\n```bash\nln -s ~/Projects/myapi ~/.coulson/myapi\n```\n\n```bash\ncurl -i http://myapi.coulson.local:18080/\n```\n\nFirst 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.\n\n### Procfile App\n\nProjects with a `Procfile` (or `Procfile.dev`) containing a `web:` process are auto-managed:\n\n```\n~/Projects/myapp/\n  Procfile            # web: bundle exec rails server -p $PORT\n```\n\n```bash\nln -s ~/Projects/myapp ~/.coulson/myapp\n```\n\n```bash\ncurl -i http://myapp.coulson.local:18080/\n```\n\nFirst request allocates a free port via `$PORT`, runs the `web` command, and proxies traffic. `Procfile.dev` takes priority over `Procfile` when both exist.\n\nTo start companion processes (workers, etc.) alongside the web process, add to `.coulsonrc`:\n\n```\nCOULSON_MANAGED_SERVICES=web,worker\n```\n\nAll listed process types from the Procfile are started together and share the same lifecycle — idle timeout reaps the entire group.\n\n### Docker Compose App\n\nProjects with a `compose.yml` (or `docker-compose.yml`) are auto-managed:\n\n```\n~/Projects/myapp/\n  compose.yml         # services: web: ...\n```\n\n```bash\nln -s ~/Projects/myapp ~/.coulson/myapp\n```\n\n```bash\ncurl -i http://myapp.coulson.local:18080/\n```\n\nFirst 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.\n\n### Static Directory\n\nProjects with a `public` subdirectory are automatically served as static files:\n\n```\n~/Projects/docs/\n  public/\n    index.html\n    style.css\n```\n\n```bash\nln -s ~/Projects/docs ~/.coulson/docs\n```\n\n```bash\ncurl -i http://docs.coulson.local:18080/\n```\n\nChanges are picked up automatically within 2 seconds.\n\n### `.coulsonrc` — Per-App Environment\n\nAdd a `.coulsonrc` file to any managed app directory to set environment variables:\n\n```\n# ~/Projects/myapp/.coulsonrc\nPORT=4000\nDATABASE_URL=postgres://localhost/myapp_dev\n```\n\nWhen `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.\n\n### `.coulson.toml` — Per-App Configuration\n\nFor full control, add a `.coulson.toml` in the app directory:\n\n```toml\nname = \"myapp\"\ndomain = \"myapp\"            # prefix only, suffix appended at runtime\nkind = \"asgi\"               # asgi, node, procfile, docker\n\n# Process\nmodule = \"mymodule:app\"     # ASGI module\nserver = \"uvicorn\"          # ASGI server\n\n# Proxy options\nport = 5006\ntimeout = 5000\ncors = false\nspa = false\n\n# Remote env injection (fetched before each cold start)\nenv_url = \"https://vault.example.com/env/myapp\"\nenv_url_headers = { Authorization = \"Bearer xxx\" }\n\n# Environment variables\n[env]\nDATABASE_URL = \"postgres://localhost/myapp_dev\"\n\n# Multi-route\n[[routes]]\npath = \"/api\"\ntarget = \"127.0.0.1:3000\"\ntimeout = 30000\n\n# Lifecycle hooks\n[hooks]\n[hooks.app_ready]\nrun = \"mise run db:migrate\"\nwebhook = \"https://hooks.slack.com/xxx\"\n```\n\n### Global Hooks\n\nCoulson fires hooks on app lifecycle events. Global hooks are executable scripts in `~/.coulson/hooks/`:\n\n```\n~/.coulson/hooks/\n  app_ready             # runs when any app becomes ready\n  app_stop              # runs when any app stops\n  scan_complete         # runs after directory scan\n```\n\nPer-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.\n\nPer-app events: `app_add`, `app_remove`, `app_start`, `app_ready`, `app_stop`, `app_idle`, `tunnel_start`, `tunnel_stop`. Global-only events: `scan_complete`.\n\n## Cloudflare Tunnel\n\nStart/stop tunnels via CLI:\n\n```bash\ncoulson tunnel start myapp\ncoulson tunnel stop myapp\n```\n\nAlso available via the Web Dashboard or the menu bar app.\n\n### Quick Tunnel\n\nNo configuration needed — assigns a random `*.trycloudflare.com` URL, great for ad-hoc sharing. Requires `cloudflared`:\n\n```bash\nbrew install cloudflared\n```\n\n### Named Tunnel (recommended)\n\nConfigure wildcard DNS for your own domain (e.g. `*.example.com`) pointing to a Cloudflare Tunnel. Coulson automatically routes subdomains to local projects:\n\n- `myapp.example.com` → local `myapp`\n- `hello.example.com` → local `hello`\n\nAll projects share one Tunnel connection — no per-app setup needed, new projects are instantly accessible from the public internet.\n\n## Management\n\n- **Web Dashboard**: `http://coulson.local:18080`\n- **CLI**: `coulson ls`, `coulson add`, `coulson restart`, `coulson open`\n- **Menu bar app**: Coulson.app menu bar icon\n\n## Configuration\n\nSupports TOML config file (`~/.config/coulson/config.toml`) and environment variables. See [example](config.example.toml).\n\nPriority: defaults \u003c config file \u003c environment variables.\n\n## Built With\n\n- [Rust](https://www.rust-lang.org/) + [Pingora](https://github.com/cloudflare/pingora) (reverse proxy)\n- [Swift](https://www.swift.org/) (macOS menu bar app)\n- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) (public sharing)\n\n## Disclaimer\n\nThis 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/).\n\n## License\n\nSee [LICENSE](LICENSE) for details.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fratazzi%2Fcoulson","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fratazzi%2Fcoulson","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fratazzi%2Fcoulson/lists"}