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/goclient, so a Go server shares its language and toolchain. - The spec comes from the code. huma emits
openapi.yamlfrom 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/capturesare recorded from the Go server and compared byte for byte on everygo test. Each response is validated against the emitted document, as is each recorded request, and every documented operation has a capture. - The Duration
patternis checked in a JavaScript engine inpackages/protocol, because the document’spatternis 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/protocoland 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
/healthexisted only in the Node server and went with it; the Go probe holds one context for the whole check. apps/server/.envis no longer read.pnpm dev:serverwarns when it finds one.