Files
quicproquo/docs/src/protocol-layers/capn-proto.md
Christian Nennemann d073f614b3 docs: rewrite mdBook documentation for v2 architecture
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.
2026-03-04 22:02:31 +01:00

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:

  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/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:

  1. No crypto: Key material never enters this crate. All encryption and signing happens in quicproquo-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