foliot docs

Decisions / 0005 · The server is Go

ADR 0005: The server is Go; the Node engine is retired

Status: accepted · 2026-10-06 · supersedes ADR 0004

Context

ADR 0004 chose Fastify and pg for the engine. The server was ported to Go in phases (tasks/todo.md, Phase 6), with the Node server kept as the reference until the Go one could stand in for it, because two schedulers claiming the same runs is a correctness problem and not a deployment one. That left one thing the Node server still did: it authored openapi.yaml, and the committed captures that pin the wire contract were replayed by its test suite.

Why Go

Four reasons, confirmed by the founder on 2026-10-06. None is a measurement:

  • Go consumers. Instastack’s control plane is Go and consumes Foliot through the generated sdk/go client, so a Go server shares its language and toolchain.
  • The spec comes from the code. huma emits openapi.yaml from the handler declarations, so the contract cannot drift from the server. The Node server’s document was written by hand and kept honest by tests.
  • Operations. A single static binary and a smaller image, without the Node runtime and node_modules.
  • Concurrency model. Goroutines and context cancellation fit the scheduler, leases and dispatch.

Decision

The Go server in go/ is the server. It authors openapi.yaml, emitted by go run ./cmd/openapi from the declarations that serve requests, so a change to a route, a struct tag or a schema cannot be made without showing up as a diff in that file; CI regenerates it and fails on a moved tree. The choices that carry the design are those recorded in Phase 6: huma for HTTP (it emits OpenAPI), pgx with sqlc for storage (the SQL is where the correctness lives, and sqlc keeps it visible SQL, as ADR 0004 wanted).

apps/server is deleted. It stays readable at the git tag node-server-final, the last commit that contains it, so the “Ported from apps/server/src/…” references in the Go code resolve.

How parity was held, and what stands in for the Node suite

  • The 92 captures in go/internal/acceptance/captures are recorded from the Go server and compared byte for byte on every go test. Each response is validated against the emitted document, as is each recorded request, and every documented operation has a capture.
  • The Duration pattern is checked in a JavaScript engine in packages/protocol, because the document’s pattern is ECMA-262 and the Go server enforces it with RE2.
  • The Node engine and wire-shape suites have Go counterparts; PR #47 holds the mapping.

Consequences

  • One server, one document, one place a contract change is authored.
  • @foliot/protocol and the TypeScript SDK remain: they are the contract for handlers and clients, not the server. Their request schemas that only the Node server used are dead code, removed separately.
  • A pool-saturation heuristic on /health existed only in the Node server and went with it; the Go probe holds one context for the whole check.
  • apps/server/.env is no longer read. pnpm dev:server warns when it finds one.