|
| 1 | +# httpsdev v0.1 — Design Spec |
| 2 | + |
| 3 | +**Date:** 2026-07-21 |
| 4 | +**Status:** Approved for planning |
| 5 | +**Owner:** @ArpitRajputGithub |
| 6 | + |
| 7 | +## Purpose |
| 8 | + |
| 9 | +One command that gives any local dev server a browser-trusted HTTPS front end. Solves the recurring "how do I run HTTPS locally" pain that every framework re-invents differently, and does it without config files, framework detection, or reverse-proxy DSLs. |
| 10 | + |
| 11 | +## Pitch |
| 12 | + |
| 13 | +``` |
| 14 | +$ mkcert -install # one time, system-wide |
| 15 | +$ httpsdev 5173 # https://localhost:3443 → http://localhost:5173 |
| 16 | +``` |
| 17 | + |
| 18 | +Two commands. Works with any HTTP dev server (Vite, Next, Rails, Django, Flask, Rust, Go, whatever). |
| 19 | + |
| 20 | +## Non-Goals (v0.1) |
| 21 | + |
| 22 | +Explicitly out of scope. Each becomes a GitHub issue for future consideration; none block v0.1. |
| 23 | + |
| 24 | +- Child-process wrapping (`httpsdev npm run dev`). |
| 25 | +- Multi-service reverse proxy with hostnames (`api.dev.local`, `web.dev.local`) — Caddy already covers this space. |
| 26 | +- Framework auto-detection (reading `package.json`, parsing stdout for ports). |
| 27 | +- Windows-first testing (build works via `GOOS=windows`; correctness testing is macOS + Linux only). |
| 28 | +- Systemd / launchd daemon mode. |
| 29 | +- Metrics endpoint (`/metrics`, Prometheus). |
| 30 | +- Web UI dashboard. |
| 31 | +- Custom CA — we shell out to mkcert instead of embedding one. |
| 32 | + |
| 33 | +## Architecture |
| 34 | + |
| 35 | +Single Go binary. Three responsibilities, executed in order at startup: |
| 36 | + |
| 37 | +### 1. Cert acquisition |
| 38 | + |
| 39 | +- Cert cache location: `~/.config/httpsdev/certs/localhost.pem` and `localhost.key` (respects `XDG_CONFIG_HOME`). |
| 40 | +- On startup: if cert files are missing OR their mtime is older than 30 days, shell out to `mkcert` to regenerate: |
| 41 | + ``` |
| 42 | + mkcert -cert-file <cache>/localhost.pem \ |
| 43 | + -key-file <cache>/localhost.key \ |
| 44 | + localhost 127.0.0.1 ::1 |
| 45 | + ``` |
| 46 | +- If `--host <name>` is passed, add it to the mkcert SAN list. |
| 47 | +- If `mkcert` is not on `PATH`: exit 1 with message `mkcert not found. Install: brew install mkcert && mkcert -install`. |
| 48 | + |
| 49 | +### 2. TLS server |
| 50 | + |
| 51 | +- `net/http.Server` with `crypto/tls.Config` loading the cached PEM + key. |
| 52 | +- Default listen port: `3443` (override with `--listen <port>`). |
| 53 | +- `MinVersion: tls.VersionTLS12`. |
| 54 | +- Bind address: `127.0.0.1` by default (safe — nothing on the LAN can reach your dev server). Pass `--lan` to bind on `0.0.0.0` for on-device mobile testing over wifi. |
| 55 | +- If the listen port is already bound: exit 1 with `port <n> already in use — try --listen <other>`. |
| 56 | + |
| 57 | +### 3. Reverse proxy |
| 58 | + |
| 59 | +- `httputil.NewSingleHostReverseProxy` targeting `http://127.0.0.1:<target-port>`. |
| 60 | +- WebSocket + SSE work out of the box via stdlib's built-in `Hijacker` path (Go 1.22+). |
| 61 | +- Preserves `Host` header from the client. |
| 62 | +- On upstream connection error: return HTTP 502 with a small plain-text body (`upstream unreachable at localhost:<n>`). |
| 63 | +- Do NOT retry — the dev server may just not be up yet; retry loops mask real errors. |
| 64 | + |
| 65 | +## CLI Surface |
| 66 | + |
| 67 | +``` |
| 68 | +httpsdev <target-port> [flags] |
| 69 | +
|
| 70 | +Flags: |
| 71 | + --listen <port> HTTPS listen port (default: 3443) |
| 72 | + --host <name> Additional SAN for cert (default: none, cert is for localhost only) |
| 73 | + --lan Bind on 0.0.0.0 for LAN (default: false, binds 127.0.0.1) |
| 74 | + --tui Full-screen dashboard mode (default: false, plain log mode) |
| 75 | + --version Print version and exit |
| 76 | + --help Print help and exit |
| 77 | +``` |
| 78 | + |
| 79 | +Four functional flags. That's the entire surface. |
| 80 | + |
| 81 | +## Output Modes |
| 82 | + |
| 83 | +### Default: plain log mode |
| 84 | + |
| 85 | +Prints a startup banner to stderr, then one line per request to stdout. Fully pipeable (`httpsdev 5173 | grep 500`). |
| 86 | + |
| 87 | +**Startup banner (stderr):** |
| 88 | +``` |
| 89 | + ▲ httpsdev v0.1.0 |
| 90 | +
|
| 91 | + ➜ Local: https://localhost:3443 |
| 92 | + ➜ Upstream: http://localhost:5173 |
| 93 | + ➜ Cert: mkcert · valid 30 days |
| 94 | +
|
| 95 | + press Ctrl+C to quit |
| 96 | +``` |
| 97 | + |
| 98 | +**Per-request line (stdout), colorized when isatty:** |
| 99 | +``` |
| 100 | +GET / 200 12ms |
| 101 | +POST /api/login 401 45ms |
| 102 | +GET /favicon.svg 404 1ms |
| 103 | +``` |
| 104 | + |
| 105 | +Colors: green for 2xx, yellow for 3xx, red for 4xx/5xx. Suppressed automatically when stdout is piped. |
| 106 | + |
| 107 | +**On Ctrl+C (stderr):** |
| 108 | +``` |
| 109 | +served 47 requests · 2 errors · avg 8ms · uptime 3m21s |
| 110 | +``` |
| 111 | + |
| 112 | +### Optional: `--tui` dashboard mode |
| 113 | + |
| 114 | +Full-screen TUI via `bubbletea` + `lipgloss`. Alt-buffer, restored on exit. |
| 115 | + |
| 116 | +Layout: |
| 117 | +- Top panel: live stats (req/s, p50/p95 latency, error rate, uptime). |
| 118 | +- Middle panel: scrolling request feed, last 20 requests, colored by status. |
| 119 | +- Bottom bar: hotkeys (`q` quit, `c` clear feed, `1`/`2` tabs). |
| 120 | +- Tabs: `1` = requests, `2` = cert info (subject, issuer, expiry). |
| 121 | +- Refresh at 10 FPS (adjustable via internal constant, not exposed as flag). |
| 122 | + |
| 123 | +**Purpose:** README hero GIF. Not a user-facing default because TUIs break pipes and copy-paste. |
| 124 | + |
| 125 | +## Data Flow |
| 126 | + |
| 127 | +``` |
| 128 | +browser ──HTTPS──▶ httpsdev:3443 ──HTTP──▶ dev-server:5173 |
| 129 | + │ |
| 130 | + └── mkcert-signed cert for localhost (+ optional --host SAN) |
| 131 | +``` |
| 132 | + |
| 133 | +Bidirectional streaming preserved end-to-end (WebSocket for Vite HMR, SSE for Next dev overlay, etc.). |
| 134 | + |
| 135 | +## Error Handling |
| 136 | + |
| 137 | +| Condition | Behavior | |
| 138 | +|-----------|----------| |
| 139 | +| `mkcert` not on PATH | Exit 1, one-line install instruction. | |
| 140 | +| mkcert exec fails | Exit 1, print mkcert's stderr. | |
| 141 | +| Cert file unreadable | Exit 1, print path + errno. | |
| 142 | +| Listen port already bound | Exit 1, suggest `--listen`. | |
| 143 | +| Target port not reachable at startup | Warn to stderr, keep running. Dev server may start later. | |
| 144 | +| Upstream connection refused during a request | HTTP 502 with plain-text body. | |
| 145 | +| Upstream timeout | Default Go proxy behavior (504-ish). No custom timeout logic in v0.1. | |
| 146 | +| SIGINT / SIGTERM | Graceful shutdown: stop accepting, drain in-flight, print summary line, exit 0. | |
| 147 | + |
| 148 | +No retries, no circuit breakers, no logging library. Stdlib `log` to stderr. |
| 149 | + |
| 150 | +## File Layout |
| 151 | + |
| 152 | +``` |
| 153 | +httpsdev/ |
| 154 | +├── main.go # cli parsing, startup, wiring |
| 155 | +├── proxy.go # TLS server + reverse proxy + request logger |
| 156 | +├── cert.go # mkcert shell-out + cache check |
| 157 | +├── ui_log.go # default log-mode banner + per-request output |
| 158 | +├── ui_tui.go # bubbletea TUI (--tui mode) |
| 159 | +├── proxy_test.go # integration test: dummy upstream → proxy → https client |
| 160 | +├── cert_test.go # cert cache + expiry logic (unit) |
| 161 | +├── go.mod |
| 162 | +├── go.sum |
| 163 | +├── README.md |
| 164 | +├── LICENSE # MIT |
| 165 | +├── .goreleaser.yaml |
| 166 | +└── .github/workflows/release.yml # GoReleaser on tag push |
| 167 | +``` |
| 168 | + |
| 169 | +Six `.go` files at ~100-200 LOC each. No packages, no interfaces, no config struct. Split when a file crosses 300 LOC — not before. |
| 170 | + |
| 171 | +## Testing |
| 172 | + |
| 173 | +**Two tests total, both stdlib-only.** |
| 174 | + |
| 175 | +1. **`proxy_test.go` — integration.** Start a dummy `httptest.NewServer` upstream. Generate a self-signed cert inline (bypasses mkcert dependency in CI). Start `httpsdev`'s proxy against the dummy. Make an HTTPS request through it with `InsecureSkipVerify` (test-only). Assert body round-trips and status code passes through. Covers cert loading, TLS handshake, and proxy forwarding in one go. |
| 176 | + |
| 177 | +2. **`cert_test.go` — unit.** Test the "should we regenerate?" logic: missing files → yes; files younger than 30 days → no; files older than 30 days → yes. Uses a temp dir, no shell-out. |
| 178 | + |
| 179 | +No test framework, no fixtures, no mocks. `go test ./...` runs both. |
| 180 | + |
| 181 | +## Release / Distribution |
| 182 | + |
| 183 | +- **GoReleaser** on git tag `v*` → GitHub Release with prebuilt binaries: |
| 184 | + - macOS: arm64, amd64 |
| 185 | + - Linux: arm64, amd64 |
| 186 | + - Windows: amd64 (untested but built) |
| 187 | +- **`go install github.com/ArpitRajputGithub/httpsdev@latest`** works from day 1. |
| 188 | +- **Homebrew tap** deferred to v0.2 (adds release-pipeline complexity for weekend v0.1). |
| 189 | + |
| 190 | +## Dependencies |
| 191 | + |
| 192 | +Direct, all pinned in `go.mod`: |
| 193 | + |
| 194 | +| Dep | Why | Alternative considered | |
| 195 | +|-----|-----|------------------------| |
| 196 | +| `github.com/fatih/color` | Colorized log-mode output | Raw ANSI codes — rejected: not worth the pixel-fiddling for one file. | |
| 197 | +| `github.com/charmbracelet/bubbletea` | TUI framework for `--tui` | `tview` — bubbletea has better ecosystem, better docs, more stars = signal for recruiters. | |
| 198 | +| `github.com/charmbracelet/lipgloss` | Styling for the TUI | Ships with bubbletea ecosystem. | |
| 199 | + |
| 200 | +Total binary size target: **< 15 MB** stripped. Bubbletea is the biggest contributor. |
| 201 | + |
| 202 | +Stdlib only: `net/http`, `net/http/httputil`, `crypto/tls`, `os/exec`, `flag`, `context`. |
| 203 | + |
| 204 | +## Success Criteria for v0.1 |
| 205 | + |
| 206 | +- `httpsdev 5173` proxies a running Vite dev server, browser shows a green padlock, HMR (WebSocket) works. |
| 207 | +- `--tui` mode produces a screenshot-worthy dashboard that renders correctly in `asciinema` recordings. |
| 208 | +- Both tests pass in CI (GitHub Actions) on macOS + Linux. |
| 209 | +- Release binaries downloadable from GitHub Releases page. |
| 210 | +- README has: one-paragraph pitch, animated GIF of `--tui` mode, install instructions, one usage example. |
| 211 | + |
| 212 | +## Future (v0.2+, not built now) |
| 213 | + |
| 214 | +Filed as issues on the repo when v0.1 ships. Each is a candidate feature, none are commitments. |
| 215 | + |
| 216 | +- Child-process wrapping (`httpsdev -- npm run dev`). |
| 217 | +- Multi-upstream config file for `api.dev.local` / `web.dev.local` routing. |
| 218 | +- Homebrew tap. |
| 219 | +- Framework auto-detect (port-from-package.json). |
| 220 | +- Embedded local CA (drop mkcert dependency). |
| 221 | +- LAN QR code (print QR of `https://<lan-ip>:3443` for mobile-device testing). |
0 commit comments