ADR 0005: tRPC as the primary control plane; MCP, REST and MQTT are additive
Status: Accepted Date: 2026-10-30
Context
sleepypod ’s control plane is tRPC with an OpenAPI facade, an MCP server built over the tRPC caller, and an MQTT bridge for Home Assistant. The iOS app and web UI are tRPC clients. That shape worked well for “extending displays and control planes” and is what we want to reuse.
Decision
src/server/routers/app.tsis the rootappRouter; every capability is a procedure there first.- REST is generated from the same procedures with
trpc-to-openapiat/api/*; every exposed procedure declares.input()and.output()zod schemas (the generator requires them; a contract test asserts the spec builds). - MCP (
src/mcp/) calls the tRPC caller; tools are named read/write primitives with units and declared limits (ADR-0011). - Live updates are a tRPC subscription over SSE (
events.onChange) fed by the process event bus (ADR-0015). No WebSocket server unless a high-rate stream needs one. superjsontransformer;publicProcedureonly (LAN-only trust model).
Consequences
- One place to add a feature; three surfaces get it.
- Subscriptions in the browser demo are served by the in-page caller
(
src/demo/link.ts).
Authors: @ng
Amendment (2026-10-30): WebSocket event stream
GET /api/ws streams the same CoreEvent frames as the SSE subscription,
prefixed by a hello frame carrying the current HAL snapshot. It exists for
clients that cannot speak tRPC — wall dashboards, an M5 dial, shell scripts —
and mirrors sleepypod ’s :3001 sensor stream on the same port via
@hono/node-ws. It is read-only: commands still go through tRPC, REST or
MQTT so the safety path has one entry.
Sources: sleepypod/core (conventions and ADR precedents) · Model Hardware Standard research preview
Imported from docs/adr/0005-trpc.md in seedypod/core. Edit it there, not here.