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>
8.6 KiB
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.ServeMuxto 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.sqlis the canonical DDL. Applied idempotently on first start bymigrate.go.models.godefines the Go structs (Endpoint,Request).repository.godefines theRepositoryinterface. The HTTP layer depends on this interface, not on the SQLite implementation.sqlite_repo.gois the concrete implementation. Usesmodernc.org/sqlite(pure Go, no CGO).db.goopens 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.Requestthat 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
chanand amap[endpointID][]chanis the entire implementation for v0.1.
Request lifecycle
- 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.
- 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
EventSourceclient that subscribes to/e/{endpoint-id}/events.
- Calls
- 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. - Mock —
PUT /e/{endpoint-id}/mockaccepts{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.
- 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
Repositoryinterface 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
Requeststruct 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_idis already first-class. Atenant_idcolumn 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.