# Protobuf Framing quicprochat v2 uses a custom binary framing protocol layered over QUIC bidirectional streams. Message payloads are serialised with Protocol Buffers (Protobuf) via the `prost` crate. The framing layer (implemented in `quicprochat-rpc`) adds a compact fixed-size header that carries the method ID, request correlation ID, and payload length -- enabling zero-copy dispatch without a separate length-delimited codec. This page covers the three frame types, the method ID dispatch table, status codes, push event delivery, and the Protobuf schema organisation. --- ## Frame Types There are three frame types in the v2 protocol. All multi-byte integers are **big-endian** (network byte order). ### Request Frame (client -> server) Sent on a QUIC bidirectional stream (one stream per RPC call): ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | method_id (u16 BE) | request_id (u32 BE) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | request_id (cont.) | payload_len (u32 BE) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | payload_len (cont.) | protobuf payload ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` | Field | Type | Bytes | Description | |---|---|---|---| | `method_id` | `u16` | 0-1 | RPC method identifier (see method IDs table) | | `request_id` | `u32` | 2-5 | Client-generated correlation ID; echoed back in the response | | `payload_len` | `u32` | 6-9 | Length of the Protobuf payload in bytes | | payload | bytes | 10+ | Protobuf-encoded request message | Header size: **10 bytes**. Maximum payload: **4 MiB**. ### Response Frame (server -> client) Sent on the same QUIC bidirectional stream as the request: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | status (u8) | request_id (u32 BE) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | request_id (cont.) | payload_len (u32 BE) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | payload_len (cont.) | protobuf payload ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` | Field | Type | Bytes | Description | |---|---|---|---| | `status` | `u8` | 0 | Status code (see status codes table) | | `request_id` | `u32` | 1-4 | Echoes the `request_id` from the request frame | | `payload_len` | `u32` | 5-8 | Length of the Protobuf payload in bytes | | payload | bytes | 9+ | Protobuf-encoded response message (may be empty on error) | Header size: **9 bytes**. ### Push Frame (server -> client, uni-stream) Sent by the server on QUIC uni-directional streams for real-time event delivery. No request ID -- push frames are not correlated to any client request. ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | event_type (u16 BE) | payload_len (u32 BE) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | payload_len (cont.) | protobuf payload ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` | Field | Type | Bytes | Description | |---|---|---|---| | `event_type` | `u16` | 0-1 | Push event type (see push event types table) | | `payload_len` | `u32` | 2-5 | Length of the Protobuf payload in bytes | | payload | bytes | 6+ | Protobuf-encoded push event message | Header size: **6 bytes**. --- ## Status Codes The `status` byte in a Response frame carries one of the following values: | Value | `RpcStatus` variant | Meaning | |-------|---------------------|---------| | 0 | `Ok` | Success. Response payload contains the result. | | 1 | `BadRequest` | Malformed request, missing required field, or failed validation. | | 2 | `Unauthorized` | Missing or invalid session token. | | 3 | `Forbidden` | Valid token but insufficient permissions for this operation. | | 4 | `NotFound` | Requested resource does not exist (e.g., KeyPackage not found). | | 5 | `RateLimited` | Request rate limit exceeded. Client should back off before retrying. | | 8 | `DeadlineExceeded` | Server could not complete the request within the configured deadline. | | 9 | `Unavailable` | Server temporarily unable to serve the request (e.g., storage unavailable). | | 10 | `Internal` | Unexpected server-side error. | | 11 | `UnknownMethod` | The `method_id` in the request is not registered. | --- ## Method IDs All 44 RPC method IDs are defined in `crates/quicprochat-proto/src/lib.rs` in the `method_ids` module. The numeric ranges group related methods by service category. ### Auth (100-103) | ID | Method | Request type | Response type | |---|---|---|---| | 100 | `OpaqueRegisterStart` | `OpaqueRegisterStartRequest` | `OpaqueRegisterStartResponse` | | 101 | `OpaqueRegisterFinish` | `OpaqueRegisterFinishRequest` | `OpaqueRegisterFinishResponse` | | 102 | `OpaqueLoginStart` | `OpaqueLoginStartRequest` | `OpaqueLoginStartResponse` | | 103 | `OpaqueLoginFinish` | `OpaqueLoginFinishRequest` | `OpaqueLoginFinishResponse` | ### Delivery (200-205) | ID | Method | Request type | Response type | |---|---|---|---| | 200 | `Enqueue` | `EnqueueRequest` | `EnqueueResponse` | | 201 | `Fetch` | `FetchRequest` | `FetchResponse` | | 202 | `FetchWait` | `FetchWaitRequest` | `FetchWaitResponse` | | 203 | `Peek` | `PeekRequest` | `PeekResponse` | | 204 | `Ack` | `AckRequest` | `AckResponse` | | 205 | `BatchEnqueue` | `BatchEnqueueRequest` | `BatchEnqueueResponse` | ### Keys (300-304) | ID | Method | Request type | Response type | |---|---|---|---| | 300 | `UploadKeyPackage` | `UploadKeyPackageRequest` | `UploadKeyPackageResponse` | | 301 | `FetchKeyPackage` | `FetchKeyPackageRequest` | `FetchKeyPackageResponse` | | 302 | `UploadHybridKey` | `UploadHybridKeyRequest` | `UploadHybridKeyResponse` | | 303 | `FetchHybridKey` | `FetchHybridKeyRequest` | `FetchHybridKeyResponse` | | 304 | `FetchHybridKeys` | `FetchHybridKeysRequest` | `FetchHybridKeysResponse` | ### Channel (400) | ID | Method | Request type | Response type | |---|---|---|---| | 400 | `CreateChannel` | `CreateChannelRequest` | `CreateChannelResponse` | ### Group Management (410-413) | ID | Method | Request type | Response type | |---|---|---|---| | 410 | `RemoveMember` | `RemoveMemberRequest` | `RemoveMemberResponse` | | 411 | `UpdateGroupMetadata` | `UpdateGroupMetadataRequest` | `UpdateGroupMetadataResponse` | | 412 | `ListGroupMembers` | `ListGroupMembersRequest` | `ListGroupMembersResponse` | | 413 | `RotateKeys` | `RotateKeysRequest` | `RotateKeysResponse` | ### Moderation (420-424) | ID | Method | Request type | Response type | |---|---|---|---| | 420 | `ReportMessage` | `ReportMessageRequest` | `ReportMessageResponse` | | 421 | `BanUser` | `BanUserRequest` | `BanUserResponse` | | 422 | `UnbanUser` | `UnbanUserRequest` | `UnbanUserResponse` | | 423 | `ListReports` | `ListReportsRequest` | `ListReportsResponse` | | 424 | `ListBanned` | `ListBannedRequest` | `ListBannedResponse` | ### User / Identity (500-501) | ID | Method | Request type | Response type | |---|---|---|---| | 500 | `ResolveUser` | `ResolveUserRequest` | `ResolveUserResponse` | | 501 | `ResolveIdentity` | `ResolveIdentityRequest` | `ResolveIdentityResponse` | ### Key Transparency (510-520) | ID | Method | Request type | Response type | |---|---|---|---| | 510 | `RevokeKey` | `RevokeKeyRequest` | `RevokeKeyResponse` | | 511 | `CheckRevocation` | `CheckRevocationRequest` | `CheckRevocationResponse` | | 520 | `AuditKeyTransparency` | `AuditKeyTransparencyRequest` | `AuditKeyTransparencyResponse` | ### Blob Storage (600-601) | ID | Method | Request type | Response type | |---|---|---|---| | 600 | `UploadBlob` | `UploadBlobRequest` | `UploadBlobResponse` | | 601 | `DownloadBlob` | `DownloadBlobRequest` | `DownloadBlobResponse` | ### Device Management (700-710) | ID | Method | Request type | Response type | |---|---|---|---| | 700 | `RegisterDevice` | `RegisterDeviceRequest` | `RegisterDeviceResponse` | | 701 | `ListDevices` | `ListDevicesRequest` | `ListDevicesResponse` | | 702 | `RevokeDevice` | `RevokeDeviceRequest` | `RevokeDeviceResponse` | | 710 | `RegisterPushToken` | `RegisterPushTokenRequest` | `RegisterPushTokenResponse` | ### Recovery (750-752) | ID | Method | Request type | Response type | |---|---|---|---| | 750 | `StoreRecoveryBundle` | `StoreRecoveryBundleRequest` | `StoreRecoveryBundleResponse` | | 751 | `FetchRecoveryBundle` | `FetchRecoveryBundleRequest` | `FetchRecoveryBundleResponse` | | 752 | `DeleteRecoveryBundle` | `DeleteRecoveryBundleRequest` | `DeleteRecoveryBundleResponse` | ### P2P and Health (800-802) | ID | Method | Request type | Response type | |---|---|---|---| | 800 | `PublishEndpoint` | `PublishEndpointRequest` | `PublishEndpointResponse` | | 801 | `ResolveEndpoint` | `ResolveEndpointRequest` | `ResolveEndpointResponse` | | 802 | `Health` | `HealthRequest` | `HealthResponse` | ### Federation (900-905) | ID | Method | Request type | Response type | |---|---|---|---| | 900 | `RelayEnqueue` | `RelayEnqueueRequest` | `RelayEnqueueResponse` | | 901 | `RelayBatchEnqueue` | `RelayBatchEnqueueRequest` | `RelayBatchEnqueueResponse` | | 902 | `ProxyFetchKeyPackage` | `ProxyFetchKeyPackageRequest` | `ProxyFetchKeyPackageResponse` | | 903 | `ProxyFetchHybridKey` | `ProxyFetchHybridKeyRequest` | `ProxyFetchHybridKeyResponse` | | 904 | `ProxyResolveUser` | `ProxyResolveUserRequest` | `ProxyResolveUserResponse` | | 905 | `FederationHealth` | `FederationHealthRequest` | `FederationHealthResponse` | ### Account (950) | ID | Method | Request type | Response type | |---|---|---|---| | 950 | `DeleteAccount` | `DeleteAccountRequest` | `DeleteAccountResponse` | --- ## Push Event Types Server-to-client push events are delivered on QUIC uni-streams using the Push frame format. Event types are defined alongside method IDs in `quicprochat-proto/src/lib.rs`: | Value | Event | Description | |-------|-------|-------------| | 1000 | `PushNewMessage` | A new message has been enqueued for the client. | | 1001 | `PushTyping` | A group member has started or stopped typing. | | 1002 | `PushPresence` | A contact's presence status has changed (online/offline). | | 1003 | `PushMembership` | Group membership changed (member added or removed). | Push events avoid the need for the client to long-poll `FetchWait` (202) for real-time delivery. The client can listen on a background task for incoming uni-streams and process push events independently of pending RPC calls. --- ## Stream Model Each RPC call uses a **dedicated QUIC bidirectional stream**: 1. Client opens a new bidirectional stream (`connection.open_bi()`). 2. Client encodes the request into a `RequestFrame` and writes it to the send half. 3. Client closes the send half (marks end-of-write). 4. Server reads the complete `RequestFrame` from the receive half. 5. Server processes the request and writes a `ResponseFrame` to its send half. 6. Server closes the send half. 7. Client reads the complete `ResponseFrame`. This allows many concurrent RPCs on a single QUIC connection without head-of-line blocking. --- ## Protobuf Schema Organisation All message types are defined in `proto/qpc/v1/`: | File | Contents | |---|---| | `auth.proto` | OPAQUE registration and login message types | | `common.proto` | Auth context, account deletion, shared types | | `delivery.proto` | Enqueue, Fetch, Peek, Ack, BatchEnqueue | | `keys.proto` | MLS key packages, hybrid keys | | `channel.proto` | Channel creation | | `group.proto` | Group management (remove member, metadata, rotate keys) | | `moderation.proto` | Report, ban, unban, list | | `user.proto` | User and identity resolution | | `kt.proto` | Key transparency (revoke, check, audit) | | `blob.proto` | Binary object storage | | `device.proto` | Multi-device management, push tokens | | `recovery.proto` | Account recovery bundles | | `p2p.proto` | P2P endpoints, health | | `federation.proto` | Cross-server relay | All `.proto` files use `package qpc.v1;` and are compiled to Rust at build time using `prost-build` via the `quicprochat-proto` crate's `build.rs`. The `protobuf-src` crate vendors `protoc`, so no system-wide `protoc` installation is required. Generated Rust types are accessed via: ```rust use quicprochat_proto::qpc::v1::{EnqueueRequest, FetchResponse, /* ... */}; use quicprochat_proto::method_ids::{ENQUEUE, FETCH, /* ... */}; ``` --- ## Design Constraints of `quicprochat-proto` The `quicprochat-proto` crate enforces three constraints: 1. **No crypto**: Key material never enters this crate. All encryption and signing happens in `quicprochat-core`. 2. **No I/O**: Callers own the transport. This crate only converts between bytes and types. 3. **No async**: Pure synchronous data-layer code. Async is the caller's responsibility. These constraints keep the serialisation layer thin and auditable. --- ## Further reading - [QUIC + TLS 1.3](quic-tls.md) -- The transport layer that carries these frames. - [Service Architecture](../architecture/service-architecture.md) -- How the server dispatches method IDs to handlers. - [Wire Format Reference](../wire-format/overview.md) -- Full Protobuf schema documentation. - [MLS (RFC 9420)](mls.md) -- How MLS messages are carried as opaque payloads inside Protobuf delivery messages. - [ADR-007](../design-rationale/adr-007-protobuf-migration.md) -- Design rationale for the v1 Cap'n Proto to v2 Protobuf migration.