The problem it exists for
Every Go service needs the same handful of things: TLS that isn’t weak, timeouts, health probes, authentication, security headers, retries and circuit breaking, and logging and tracing on both the server and the clients it calls. Done piecemeal, each one is another chance to get a default wrong.
Defaults really are where it bites. Measured on this stack’s own server before it was fixed, a 13-second stream delivered 10 of its 13 frames, and the access log recorded a success, with a byte count higher than the client ever received (spec 0001).
How it works
One posture, everywhere
tls sets one TLS posture (1.2 at minimum, six chosen cipher suites) under the HTTP server, the gRPC server and both client factories, and transit is one middleware module that the server and the clients share. So the logging, tracing, retries and breakers on either end of a call come from the same place.
Clients and servers apart
transport is the server side (HTTP, gRPC and a REST gateway) on top of tls, authn and transit, and httpclient and grpcclient are the client side, in separate modules, so a program that only makes calls never links server code. The heavy extras are opt-in: an embedded API-docs UI, Prometheus metrics, and localca for trusted HTTPS on your own machine. Every one of the nine fails its build if a framework, a CLI library or a cloud SDK sneaks into its dependencies.
Each guard on the side it protects
Where a guard sits is part of the design. A rate limiter protects whatever is receiving the load, so it goes on the server. A circuit breaker protects a caller from a service that’s failing, so it goes on the client, and the framework offers neither the other way round, because “putting either on the wrong side is a category error” (transport middleware). On the client, the breaker sits outside retry. One call that retries three times and still fails counts as one failure, not three, so the breaker doesn’t trip too eagerly, and once it’s open, a call is refused before any time is spent retrying (transit’s middleware model).
Decisions and what they cost
- Light clients, heavy server. The stack was split so client modules stay small, rather than one module dragging lifecycle, auth and gateway code into every program that only calls out (gtb spec 0123). What it cost: nine modules to keep in step, which grouped automated updates take care of.
- A clean break. The server stack left go-tool-base as a clean break, with no facade re-exporting the old names (gtb spec 0129). What it cost: a breaking change downstream, eased by a symbol-by-symbol migration note.
- Fix the logs, not a subsystem. The streaming problem above was fixed by repairing the logging middleware that hid failed writes, documenting how a stream outlives a timeout, and adding a small helper for server-sent events, instead of building a streaming subsystem (spec 0001). What it cost: safe streaming still depends on reading the docs.
- The body cap stays on. Every route keeps a 1 MiB request-body cap by default, and a single handler can raise it for its own request (spec 0002), because dropping the server-wide cap would quietly undo a security-audit finding. What it cost: the cap is no longer a pure wrapper.
Proof in use
- go-tool-base, keryx, krites and phpbotscout serve through it, and go/forge and its four adapters make their calls through httpclient.
- 75 releases across the nine modules since July 2026, with every consumer on the current one. Every module’s release merge request runs cicd’s go-core-currency check, which flags a release still built against an older transport core.
- Nearly 600 test functions across the stack.
Use it when, and when not to
Use it if you’re writing Go services and clients and want hardened defaults that agree with each other, without adopting a whole web framework.
It reads no config files or environment variables and reloads nothing, renewed certificates included. There’s no HTTP/2 over plain text, no Unix sockets, no mutual TLS on the gRPC client, and httpclient doesn’t negotiate HTTP/2 or retry a POST. The breakers and rate limiters don’t coordinate across replicas, authn refuses shared-secret tokens and has no revocation, and localca’s Windows trust-store install doesn’t work yet.
Where it’s going
Making a server that dies after a clean start visible to its supervisor, and masking credential names in request logs, are the next fixes.
The modules
- authn Transport-agnostic request authentication: API key, JWT/OIDC and mTLS, usable from either transport.
- grpcclient A light gRPC client dial factory: a decoupled target, go/tls credentials and the transit client interceptors.
- httpclient A hardened *http.Client factory: secure TLS defaults, downgrade-proof redirects and the transit middleware.
- localca A framework-free, mkcert-style local development CA: a per-machine root, cross-OS trust-store install and browser-trusted HTTPS on localhost and LAN IPs with zero manual setup.
- tls Hardened, framework-free TLS plumbing for Go: sensible defaults and the pieces the transports and clients build on.
- transit The shared HTTP and gRPC client middleware both factories use: retry, circuit-breaker and auth.
- transport-metrics Cardinality-safe Prometheus instrumentation: a scrapeable /metrics endpoint (Go runtime, process and build-info collectors), optional pprof, mounted on your server or standalone. The pull/scrape counterpart to the OTel observability module; go/transport, gRPC and OTel isolated in opt-in subpackages.
- transport-openapi Serve an OpenAPI spec and an interactive Stoplight Elements docs site from one Register call, mounted on your transport mux. Keeps the ~2.4 MB embedded docs UI out of servers that don't need it.
Last reviewed .