Tech Stack
Tech Stack
Decisions locked 2026-09-22 after research (plans/reports/researcher-260922-0104-*.md). Versions verified against proxy.golang.org on that date.
Decisions
| Concern | Choice | Version | Rationale |
|---|---|---|---|
| Language | Go | 1.27 | Stdlib uuid.NewV7, encoding/json/v2, GA testing/synctest, goroutine-leak profile. |
| HTTP | Standard library net/http | Go 1.27 | http.ServeMux matches methods and path parameters (Go 1.22+), and r.Pattern gives the route for span names and http.route, so otelhttp works without a framework adapter. SSE flushing and cancellation on disconnect work unchanged. Groups, central errors and JSON binding are a small router in internal/server. goclaw does the same. |
| Streaming transport | Server-Sent Events | stdlib | Client POSTs, server streams. No bidirectional need. WebSocket rejected. |
| LLM provider (first) | anthropic-sdk-go behind own Provider interface | v1.74.0 | SDK owns union decoding and stream accumulation. Learning stays on the loop. Interface is the seam for more providers. |
| LLM provider (second) | openai-go behind same interface | v3.x | Covers OpenAI, Ollama, OpenRouter, vLLM via base URL. Adapter synthesizes block start/stop. |
| Database | PostgreSQL via pgx | pgx v5.11.0 | Hand-written SQL, pgx.CollectRows. SQLite dual-dialect rejected as scope trap. |
| Migrations | goose as library, embed.FS | v3.28.0 | Embedded SQL migrations, one binary. |
| Tenancy | tenant_id NOT NULL on every table, scoped store built from context | n/a | Fail-closed. Cross-tenant read test per table. RLS with SET LOCAL as later backstop, never a replacement. |
| Identity | hango owns tenants, users, roles, API keys | n/a | No passwords or login UI. Bootstrap admin key. API keys belong to a user. |
| Run model | Durable runs row executed by scheduler lane worker, SSE subscribes | n/a | Survives disconnect, enables cancel, resume, HITL, graceful drain. Single replica in v1. |
| Messages | Append-only rows, seq number, jsonb blocks | n/a | Per-turn deltas, per-message addressing for compaction and subagents. |
| Observability | OpenTelemetry + log/slog | otel v1.46.0 | hango-local GenAI semconv constants. LLM bodies off by default. |
| Rate limiting | golang.org/x/time/rate per API key | latest | In-process, documented single-replica limitation. |
| Testing | testing, testing/synctest, testcontainers-go for Postgres | v0.44.0 | Real database in integration tests, no mocks of the store. |
| Lint | golangci-lint v2 | v2.13.2 | errcheck, govet, staticcheck, gosec, bodyclose, sqlclosecheck. |
| Container | Multi-stage Dockerfile, distroless static nonroot | n/a | CGO_ENABLED=0, compose with Postgres. |
| Web UI (follow-up) | Svelte 5 | n/a | For the web chat and the admin web page after the v1 phases. Both talk to the same HTTP API and SSE stream as any other client. |
Explicitly rejected
- Agent frameworks (eino, adk-go, genkit, langchaingo): the harness is the learning target.
- Echo, Gin, chi, Fiber, gorilla: the standard-library router covers hango’s routes, and a framework context type would spread into every handler and middleware. Echo v5 also has no maintained OpenTelemetry middleware. Fiber’s fasthttp breaks SSE flushing,
otelhttpand cancellation. chi is the fallback if the hand-written router outgrows about 200 lines, because its handlers stay plainnet/http. - sqlc, ORMs: explicit SQL is the learning surface and where tenant scoping lives.
- WebSocket, Redis, message queues: not needed at single-replica scope.
pkg/directory: nothing public until an external consumer exists.