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

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.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. MockPUT /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.
  • 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.