# 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.
