Dual Protobuf Runtime: prost (core) + buffa (server)
Date: 2026-05-19 Status: Accepted
Decision
Core and server intentionally use separate generated protobuf runtimes:
libs/coreuses prost for offline/mobile/WASM/UniFFI serializationapps/serveruses buffa for ConnectRPC transport.protofiles inproto/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:
| Layer | Proto runtime | Deployment targets |
|---|---|---|
libs/core | prost | iOS (UniFFI), Android (UniFFI), WASM (dashboard, PWA), server |
apps/server | buffa | Server 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
.protofiles — 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