Update 25+ files and add 6 new pages to reflect the v2 migration from Cap'n Proto to Protobuf framing over QUIC. Integrates SDK and Operations docs into the mdBook, restructures SUMMARY.md, and rewrites the wire format, architecture, and protocol sections with accurate v2 content.
14 KiB
Protobuf Framing
quicproquo 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 quicproquo-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/quicproquo-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 quicproquo-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:
- Client opens a new bidirectional stream (
connection.open_bi()). - Client encodes the request into a
RequestFrameand writes it to the send half. - Client closes the send half (marks end-of-write).
- Server reads the complete
RequestFramefrom the receive half. - Server processes the request and writes a
ResponseFrameto its send half. - Server closes the send half.
- 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/qpq/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 qpq.v1; and are compiled to Rust at build time using prost-build via the quicproquo-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:
use quicproquo_proto::qpq::v1::{EnqueueRequest, FetchResponse, /* ... */};
use quicproquo_proto::method_ids::{ENQUEUE, FETCH, /* ... */};
Design Constraints of quicproquo-proto
The quicproquo-proto crate enforces three constraints:
- No crypto: Key material never enters this crate. All encryption and signing happens in
quicproquo-core. - No I/O: Callers own the transport. This crate only converts between bytes and types.
- 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 -- The transport layer that carries these frames.
- Service Architecture -- How the server dispatches method IDs to handlers.
- Wire Format Reference -- Full Protobuf schema documentation.
- MLS (RFC 9420) -- How MLS messages are carried as opaque payloads inside Protobuf delivery messages.
- ADR-007 -- Design rationale for the v1 Cap'n Proto to v2 Protobuf migration.