hatch-surf/docs/engineering/hatch-architecture.md
CTO 82ccb1fc1b docs+license: realign engineering foundation to Go stack per ADR-0001/0002
Migration commit for the ELF-15 atomic PR. Realizes the CTO plan in
ELF-13 revision 1 and ADR-0001 (Go + SQLite + stdlib net/http + SSR +
SSE) and the detail choices in ADR-0002 (chi, modernc.org/sqlite, html/
template, stdlib testing, Apache-2.0).

Docs:
- docs/engineering/tech-stack.md rewritten for Go + SQLite + stdlib
  net/http + SSR + SSE. Frontend is server-rendered HTML + a little
  vanilla JS. Packaging is multi-stage golang -> scratch with an
  optional Caddy sidecar. Boring-technology principle kept.
- docs/engineering/first-engineer-role.md rewritten: Go must-have
  (stdlib + HTTP fundamentals + SQL + GitHub), TypeScript/Next.js/
  Prisma/PostgreSQL moved to nice-to-have or deferred. Tailwind and
  monorepo tooling explicitly deferred.
- docs/engineering/onboarding.md: pnpm install / pnpm dev replaced
  with go test ./... and go run ./cmd/hatch. New step: read local-dev
  and hatch-architecture on day 1.
- docs/engineering/local-dev.md (new): day-to-day commands, where
  things live, SQLite tips, troubleshooting. Living doc, engineer owns.
- docs/engineering/hatch-architecture.md (new): component map (http
  ServeMux -> handler layer -> store layer; in-process SSE hub),
  request lifecycle (Capture / Inspect / Mock / Live update), data
  model (endpoints, requests), performance budget, future seams.
- docs/adrs/0002-hatch-detail-stack.md (new): CTO-authored ADR
  closing the open choices in ADR-0001 with concrete picks, named
  alternatives, and a per-choice rollback path. Router: go-chi/chi.
  SQLite driver: modernc.org/sqlite (pure Go, no CGO). Templates:
  stdlib html/template + //go:embed. Tests: stdlib testing + httptest.
  License: Apache-2.0.

Housekeeping:
- README.md: drop the apps/ and packages/ layout lines (no
  monorepo), add local-dev and hatch-architecture pointers, fix
  Hatch demo to point at /healthz, switch license line from
  'Proprietary' to Apache-2.0 with a LICENSE file pointer.
- CONTRIBUTING.md: code-style section rewritten for Go (gofmt,
  go vet, internal/ for pure logic, server-rendered by default);
  branch example uses engineer/hatch-* matching the actual workflow.
- LICENSE: full Apache-2.0 text, copyright El Foundation 2026.
  Per ADR-0002.
- .gitignore: ignore the pre-built 'hatch' binary, bin/ (per ADR
  0002 binary convention), and SQLite files (*.db, *.db-journal,
  *.db-wal, *.db-shm).

Out of scope (handled by ELF-17 onwards, not this PR):
- Storage layer implementation (Task B, ELF-17)
- Capture, Inspect, Mock (Tasks 3-6)
- E2E smoke test (Task 8)

Foundation + this commit are the atomic PR. CI is green (go vet,
go test ./... -race, docker build all pass locally; same gates the
GitHub Actions workflow enforces).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-22 15:12:04 +02:00

145 lines
8.6 KiB
Markdown

# Hatch Architecture
Living document. The CTO sketches the component map; the engineer owns the details as the code lands.
## One-sentence summary
Hatch is a single Go binary that serves a server-rendered HTML UI and captures, inspects, and mocks HTTP requests against per-endpoint URLs, persisting everything in a single SQLite file.
## High-level component map
```
┌─────────────────────────────────────┐
│ Browser (Hatch UI) │
│ GET /e/{id} + EventSource /events│
└──────────────┬──────────────────────┘
│ HTTP / SSE
┌──────────────────────────────────────────────────────────────┐
│ hatch (Go binary) │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌───────────────┐ │
│ │ http.ServeMux│──▶│ handler layer │──▶│ store layer │ │
│ │ (stdlib) │ │ (internal/ │ │ (internal/ │ │
│ │ │ │ handler/) │ │ store/) │ │
│ └──────────────┘ └────────────────┘ └───────┬───────┘ │
│ │ │ │
│ │ ┌────────────────┐ │ │
│ └────────────▶│ SSE hub │◀─────────┘ │
│ │ (broadcast │ │
│ │ new requests)│ │
│ └────────┬───────┘ │
│ │ │
│ ┌────────▼───────┐ │
│ │ html/template │ │
│ │ (server-render)│ │
│ └────────────────┘ │
└──────────────────────────────┬───────────────────────────────┘
│ modernc.org/sqlite (pure Go)
┌──────────────┐
│ hatch.db │
│ (SQLite) │
└──────────────┘
```
## Layer responsibilities
### `cmd/hatch/main.go` — process entrypoint
- Reads configuration (env: `PORT`).
- Wires the `http.ServeMux` to the handler layer.
- Starts `http.ListenAndServe`. Logs to stdout. Crashes on bind failure.
### `internal/handler/` — HTTP layer
- One file per route group: `capture.go`, `inspect.go`, `mock.go`, `sse.go`, `health.go`.
- Handlers take a `store.Repository` (interface) — never a concrete type — so tests can swap in `:memory:` SQLite or a fake.
- Handlers translate HTTP ↔ store calls. They do not contain business logic.
- Server-rendered HTML lives next to the handler that renders it. Templates are `//go:embed`-ed at build time.
### `internal/store/` — persistence layer
- `schema.sql` is the canonical DDL. Applied idempotently on first start by `migrate.go`.
- `models.go` defines the Go structs (`Endpoint`, `Request`).
- `repository.go` defines the `Repository` interface. The HTTP layer depends on this interface, not on the SQLite implementation.
- `sqlite_repo.go` is the concrete implementation. Uses `modernc.org/sqlite` (pure Go, no CGO).
- `db.go` opens the database file (or `:memory:` for tests), configures pragmas, returns a `*sql.DB`.
### SSE hub (in `internal/handler/sse.go`)
- A goroutine per connected browser, holding an `http.Flusher`.
- A `chan store.Request` that capture handlers publish to.
- The hub fans out new requests to all subscribers for the relevant endpoint.
- No external broker. No Redis. No pubsub library. A `chan` and a `map[endpointID][]chan` is the entire implementation for v0.1.
## Request lifecycle
1. **Capture** — a webhook hits `/{endpoint-id}` (any method). The capture handler:
- Reads method, path, query, headers, body.
- Calls `store.AppendRequest(ctx, request)`.
- Publishes the new request to the SSE hub.
- Looks up the endpoint's mock response and returns it.
2. **Inspect** — a browser hits `GET /e/{endpoint-id}`. The inspect handler:
- Calls `store.ListRequests(ctx, endpointID, limit=100)`.
- Renders an HTML page with the request list (newest first).
- The page includes a small vanilla-JS `EventSource` client that subscribes to `/e/{endpoint-id}/events`.
3. **Live update** — the SSE handler holds the connection open, flushes each new request as a `data:` frame, and the browser appends it to the list.
4. **Mock**`PUT /e/{endpoint-id}/mock` accepts `{status, headers, body}` and updates the endpoint. Subsequent captures to that endpoint return the configured response.
## Data model
Two tables. That's it for v0.1.
```
endpoints
id TEXT PRIMARY KEY -- URL-safe random ID, e.g. "h7c2k"
created_at INTEGER NOT NULL -- Unix epoch seconds, UTC
mock_status INTEGER -- nullable; 200/201/204/etc.
mock_headers TEXT -- JSON object, nullable
mock_body BLOB -- nullable
requests
id INTEGER PRIMARY KEY AUTOINCREMENT
endpoint_id TEXT NOT NULL REFERENCES endpoints(id)
received_at INTEGER NOT NULL -- Unix epoch seconds, UTC
method TEXT NOT NULL
path TEXT NOT NULL -- request path within the endpoint
query TEXT NOT NULL -- raw query string
headers TEXT NOT NULL -- JSON object
body BLOB -- nullable
remote_addr TEXT -- for debugging
FOREIGN KEY (endpoint_id) REFERENCES endpoints(id) ON DELETE CASCADE
```
Index on `requests(endpoint_id, received_at DESC)` for the list page query.
## Boundaries
- **No authentication.** v0.1 is single-user, self-hosted. If you can reach the port, you can read and write. See [v0.1-scope.md](../adrs/v0-1-scope.md).
- **No multi-tenancy.** One SQLite file, one process, one operator. Cloud is v0.2+.
- **No external services.** No Redis, no Postgres, no S3. If the binary needs to phone home, the design is wrong.
- **No client-side framework.** The browser gets HTML and a 50-line `events.js`. Anything more is out of scope for v0.1.
## Performance budget
v0.1 is sized for a single developer self-hosting on a $5 VPS:
- **Cold start:** under 100 ms (Go binary + SQLite open).
- **Capture latency:** under 5 ms p99 on the happy path (no auth, no remote calls).
- **SSE fan-out:** one goroutine per connected browser, no message broker.
- **Database size:** comfortable to 100k requests per endpoint. Retention policy is out of scope for v0.1.
If a real workload breaks these, we measure first, then add complexity.
## Future seams (do not build now)
These are deliberate extension points, not planned features. The v0.1 implementation should not block them but also should not build them.
- **Pluggable storage** — the `Repository` interface is the seam. A Postgres or S3-backed implementation would slot in without handler changes.
- **Pluggable auth** — a middleware in front of the mux. The v0.1 mux is exposed without auth, which is the right v0.1 default.
- **Replay / forwarding** — the `Request` struct has the full request. Replay is "read from store, write back out." Forwarding is the same plus a destination.
- **Multi-tenant cloud** — partitioning by `endpoint_id` is already first-class. A `tenant_id` column is a v0.2 change.
When the v0.1 design cannot accommodate one of these without a rewrite, we have under-designed. When the v0.1 design has built one of these speculatively, we have over-designed.