Skip to content

ADR-0002 — Dual-port REST + gRPC for Sense API

  • Status: Accepted
  • Date: 2026-04-28

Context

Sense API serves two clients with very different ergonomics:

  • The Planner web app is a TypeScript SPA. JSON over HTTP/REST is natural; we generate the client from Swagger via Orval.
  • The Sense Mobile App is native Kotlin/Swift. Bandwidth and battery matter. gRPC + protobuf is a better fit — smaller payloads, generated typed stubs, HTTP/2 keepalives.

We could:

  1. REST only — dumb-down the mobile contract; lose protobuf's payload efficiency.
  2. gRPC only — push gRPC into the browser via grpc-web; lose Swagger / OpenAPI tooling for the web app.
  3. Two services — one REST, one gRPC. Doubles the deploys.
  4. Dual-port — one process, one DI graph, two listeners on different ports.

Decision

Run Sense API as one process with two ports: REST on 8080 (HTTP/1.1) and gRPC on 8081 (HTTP/2). Both share the same controllers' underlying business logic via the IOperation<TParam, TResult> pattern.

Consequences

  • Positive: One deployment, one set of secrets, one scaling unit. Operations staff don't have to remember which port is what.
  • Positive: Each protocol gets its own middleware stack (Kestrel pipeline configuration is per-port). REST gets output caching, JSON serializer options, OpenAPI / Swagger middleware. gRPC gets reflection, large message size config, HTTP/2 specifics. No coercion.
  • Positive: Web app codegen (Orval from Swagger) and mobile codegen (proto compilers) are independent — each surface evolves at its own pace.
  • Negative: The ingress has to route by port (or by path with backend-protocol annotations). Slightly more complex than a single-port setup.
  • Negative: The temptation to drift the two surfaces (a feature on REST only, or only in gRPC). We resist this — both surfaces should reach the same operations behind the controller layer.

Notes

The shared IOperation<TParam, TResult> layer is what makes this practical. Controllers (REST and gRPC services alike) are thin shells: authn/authz, validate, hand off to the operation, format the response. This is enforced by code review.