Skip to content

Commit b63c769

Browse files
docs: add v0.1 design spec
1 parent 2be73c8 commit b63c769

1 file changed

Lines changed: 221 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)