Dual Protobuf Runtime: prost (core) + buffa (server)

Date: 2026-05-19 Status: Accepted

Decision

Core and server intentionally use separate generated protobuf runtimes:

  • libs/core uses prost for offline/mobile/WASM/UniFFI serialization
  • apps/server uses buffa for ConnectRPC transport
  • .proto files in proto/ remain the single schema source of truth
  • Domain models (pure Rust structs in core) remain the boundary between transport and business logic

Context

When switching the server from tonic (gRPC-only) to ConnectRPC (JSON + protobuf + gRPC), the ConnectRPC Rust stack requires buffa-generated types for its service trait. The question arose: should we also migrate core from prost to buffa for a single-runtime story?

Rationale

Core and server have fundamentally different proto consumers:

LayerProto runtimeDeployment targets
libs/coreprostiOS (UniFFI), Android (UniFFI), WASM (dashboard, PWA), server
apps/serverbuffaServer containers only

Core’s prost types serve: offline storage serialization, sync queue encoding/decoding, UniFFI boundary to Swift/Kotlin, WASM compilation for web apps.

Server’s buffa types serve: ConnectRPC request/response handling with zero-copy views. These types never leave the server.

The server does not use core’s proto conversions for RPC. The handler pattern is: buffa request views → domain logic (from core) → buffa response types. Core’s pattern is: domain types ↔ prost types (for offline/sync). These are separate concerns.

Why not migrate core to buffa:

  • Prost is battle-tested on WASM, iOS/Android cross-compilation, and UniFFI. Buffa’s cross-platform story is unverified for those targets.
  • Core has 44 tests and 31 conversion impls (20 entities + 11 enums) that would need rewriting for a runtime that only the server needs.
  • The “duplication” is only in generated code (gitignored). The .proto files — the actual contract — remain singular.

Analogy: This is the same pattern as the server using sqlx types that never leak into core. Different layers have different serialization needs.

Code Generation

proto/*.proto
    │
    ├──▶ buf generate
    │       ├──▶ prost types      → libs/core/src/gen/        (messages, shared by all clients)
    │       ├──▶ buffa types      → apps/server/src/gen/proto/ (messages, server-local)
    │       └──▶ connectrpc stubs → apps/server/src/gen/rpc/   (service traits, server-local)
    │
    ├──▶ connect-swift  → apps/ios/Generated/      (Swift clients)
    └──▶ connect-kotlin → apps/android/generated/  (Kotlin clients)

Consequences

  • Developers must know which proto types to use where: core = prost, server = buffa
  • Two sets of generated Rust types exist for the same protos (both gitignored, negligible cost)
  • If buffa later proves stable on all targets and offers clear benefits, this decision can be revisited — but the migration should be driven by a concrete need, not purity