ADR 0116 — A selected Unix transport never becomes TCP
Context
createHttpClient({ unix }) passed a Bun-specific option into fetch. On Bun
that selected a socket file; elsewhere the option was not a portable Fetch
contract and the same call could reach baseUrl over TCP. A deployment boundary
must not depend on a runtime silently understanding an extension.
The transport also owns work after response headers arrive. Bounding only the JSON parser leaves unread response bytes, open sockets and ambiguous delivery outside the abstraction.
Decision
createUnixClientTransport({ socketPath }) is the explicit Fetch-compatible
adapter exported by stitchkit/server and stitchkit/node. The typed client uses
it through the existing HttpClientConfig.fetch seam:
const transport = createUnixClientTransport({ socketPath: '/run/worker.sock' })
const http = createHttpClient({ baseUrl: 'http://worker', fetch: transport.fetch })
The path is absolute, every redirect remains on that path, requests and
responses have byte ceilings, response headers have time and byte ceilings,
connections are finite, and close() owns active work. Failures report whether
dispatch did not begin, may have begun, or a response was received; the adapter
never guesses that an ambiguous operation is replay-safe.
The legacy unix option remains Bun-only for source compatibility. Selecting it
on another runtime now refuses before dispatch instead of using TCP, and it is
mutually exclusive with an injected fetch.
Consequences
- One typed-client path works on Bun and Node without importing runtime code into the browser entrypoint.
- A missing socket, redirect, timeout or close cannot cross the declared transport boundary.
- Applications own retry policy. Delivery uncertainty is data, not an implicit retry or fallback.
- The adapter is process-local plumbing; it does not discover sockets, start a daemon or authenticate callers.