---
status: accepted
date: 2026-04-07
---

# REST-First OoP Communication — Each Gear Runs Its Own HTTP Server

**ID**: `cpt-cf-adr-rest-first-oop`

## Table of Contents

<!-- generated by `cypilot toc` -->

## Context and Problem Statement

Earlier ToolKit OoP designs (see RESTOverGRPC.MD gist) proposed a REST-over-gRPC bridge pattern where OoP gears would
expose gRPC only, and a `RestBridge` layer would translate REST calls into `UnaryRequest`/`UnaryResponse` proto
messages. This approach required every OoP gear to depend on tonic and proto definitions, added a translation layer
that obscured errors, and made debugging with standard HTTP tools (curl, Postman) difficult. Should OoP gears
communicate via a gRPC bridge, native REST, or a sidecar proxy?

## Decision Drivers

* Debuggability: REST requests are inspectable with curl, browser devtools, and standard HTTP tooling. gRPC requires
  specialized tools (grpcurl, Postman gRPC).
* Language flexibility: Future non-Rust OoP gears can serve REST without adopting tonic/proto. REST is the universal
  interop protocol.
* Existing investment: OperationBuilder already generates REST routes and OpenAPI specs. Gears already define their
  public API as REST endpoints.
* Proto dependency: The gRPC bridge approach requires `UnaryRequest`/`UnaryResponse` proto definitions and a custom
  codec, adding build complexity.
* Industry alignment: Kubernetes services, API gateways (Kong, Tyk, Envoy), and most microservice frameworks (Spring
  Boot, Express, Actix-web) use REST/HTTP as the default inter-service protocol.

## Considered Options

* **Option A**: REST-over-gRPC bridge — OoP gears expose gRPC; a bridge translates REST ↔ gRPC.
* **Option B**: Native REST — each OoP gear runs its own Axum HTTP server with the full ToolKit middleware stack.
* **Option C**: Sidecar proxy (Dapr-style) — a sidecar process handles inter-gear communication; gears talk to
  localhost sidecar.

## Decision Outcome

Chosen option: "Native REST", because it leverages the existing OperationBuilder/Axum investment, requires no proto
dependency for REST gears, is debuggable with standard tools, and aligns with how the industry builds microservices.
gRPC remains available as an opt-in for performance-critical internal paths but is not required for standard gear
communication.

### Consequences

* The `UnaryRequest`/`UnaryResponse` proto pattern of the REST-over-gRPC bridge (Option A) is not adopted. No
  REST-to-gRPC translation layer is needed.
* The OoP bootstrap (`libs/toolkit/src/bootstrap/oop.rs`) must be extended to start an Axum HTTP server from the gear's
  OperationBuilder routes.
* Each OoP Worker serves its own `/openapi.json` endpoint, enabling the gateway to aggregate specs.
* Generated REST clients (`cpt-cf-component-rest-client-gen`) target HTTP endpoints, not gRPC. The Rust SDK trait is
  the single source of truth for codegen (`#[toolkit::rest_contract]`); `openapi.json` is a published output.
* gRPC remains available via grpc-hub for gears that explicitly opt in (e.g., `DirectoryService`). The two protocols
  coexist without conflict.
* Latency for REST calls is slightly higher than gRPC (HTTP/1.1 headers vs. HTTP/2 binary framing), but within the 5 ms
  p95 budget for typical payloads.

### Confirmation

* Code review: verify that OoP bootstrap starts an Axum HTTP server, not a gRPC-only server.
* Architecture review: verify that no `UnaryRequest`/`UnaryResponse` proto types exist in the codebase.
* Integration test: an OoP gear serves a REST endpoint that is callable with curl and returns a valid
  OpenAPI-documented response.

## Pros and Cons of the Options

### Option A: REST-over-gRPC Bridge

OoP gears expose gRPC only. A `RestBridge` component translates between REST and a generic `UnaryRequest`/
`UnaryResponse` proto.

* Good, because gRPC HTTP/2 binary framing is efficient for large payloads and streaming.
* Good, because a single gRPC port per gear simplifies networking.
* Bad, because requires proto compilation and tonic dependency for all OoP gears, even those that only need REST.
* Bad, because the bridge layer obscures HTTP semantics (status codes, headers, content negotiation).
* Bad, because debugging requires grpcurl or Postman gRPC mode — not standard curl.
* Bad, because OpenAPI specs cannot be served directly; they must be translated or maintained separately.
* Bad, because future non-Rust gears must adopt tonic-compatible gRPC stacks.

### Option B: Native REST

Each OoP gear runs its own Axum HTTP server. The gear's `register_rest()` / OperationBuilder routes are served
directly.

* Good, because leverages the existing OperationBuilder/Axum/Tower middleware investment — no new abstractions.
* Good, because debuggable with curl, browser devtools, Postman, and any HTTP client.
* Good, because OpenAPI spec is served directly from the gear (same as in-process behavior).
* Good, because no proto dependency required for REST-only gears.
* Good, because future non-Rust gears can serve standard HTTP.
* Good, because aligns with industry-standard microservice communication.
* Neutral, because each OoP gear needs its own port allocation (handled by bootstrap config).
* Bad, because HTTP/1.1 has slightly higher per-request overhead than gRPC HTTP/2 binary framing.

### Option C: Sidecar Proxy (Dapr-Style)

A sidecar process runs alongside each gear, handling service discovery, retries, and protocol translation.

* Good, because gears are fully agnostic to communication protocol.
* Good, because the sidecar can add observability, mTLS, and retry without gear changes.
* Bad, because adds an extra process per gear (resource overhead, operational complexity).
* Bad, because introduces latency (gear → sidecar → target sidecar → target gear = 2 extra hops).
* Bad, because the sidecar must be built and maintained — significant engineering investment.
* Bad, because on-premise Windows deployments complicate sidecar lifecycle management.
* Bad, because Dapr-style sidecars are a heavy dependency for a framework that already has toolkit-http and Tower
  middleware.

## More Information

The REST-over-gRPC bridge option (Option A) is documented in the RESTOverGRPC.MD gist. Key patterns from that approach
that are **not adopted**:

- `RestBridge` component and `RestInvoke` gRPC service
- `UnaryRequest` / `UnaryResponse` proto messages
- Generic request/response serialization through proto `bytes` fields

The following existing patterns **are preserved**:

- gRPC for `DirectoryService` (service discovery)
- gRPC for `grpc-hub` aggregation
- Optional gRPC interfaces for performance-critical gear-to-gear paths (explicitly opted in per gear)

## Traceability

- **PRD**: [PRD.md](../PRD.md)
- **DESIGN**: [DESIGN.md](../DESIGN.md)

This decision directly addresses the following requirements or design elements:

* `cpt-cf-fr-rest-primary` — REST is the primary OoP protocol by this decision
* `cpt-cf-fr-developer-transparency` — Same OperationBuilder/Axum routes work in-process and OoP without code changes
* `cpt-cf-fr-rest-client-gen` — REST clients are generated trait-first (`#[toolkit::rest_contract]`), enabled by this decision
* `cpt-cf-nfr-oop-latency` — Native REST avoids the bridge translation overhead; HTTP/1.1 overhead is within budget
* `cpt-cf-component-oop-bootstrap` — Bootstrap must start an Axum HTTP server per this decision
