# Delivery Schema **Schema file:** `schemas/delivery.capnp` **File ID:** `@0xc5d9e2b4f1a83076` The `DeliveryService` interface defines the RPC contract for the store-and-forward message relay. The DS is intentionally MLS-unaware: it routes opaque byte strings by recipient key and optional channel ID without parsing or inspecting the content. --- ## Full schema listing ```capnp # delivery.capnp -- Delivery Service RPC interface. # # The Delivery Service is a simple store-and-forward relay. It does not parse # MLS messages -- all payloads are opaque byte strings routed by recipient key. # # Callers are responsible for: # - Routing Welcome messages to the correct new member after add_members(). # - Routing Commit messages to any existing group members (other than self). # - Routing Application messages to the intended recipient(s). # # The DS indexes queues by the recipient's raw Ed25519 public key (32 bytes), # matching the indexing scheme used by the Authentication Service. # # ID generated with: capnp id @0xc5d9e2b4f1a83076; interface DeliveryService { # Enqueue an opaque payload for delivery to a recipient. # # recipientKey : Ed25519 public key of the intended recipient (exactly 32 bytes). # payload : Opaque byte string -- a TLS-encoded MlsMessageOut blob or any # other framed data the application layer wants to deliver. # channelId : Optional channel identifier (empty for legacy). A 16-byte UUID # is recommended for 1:1 channels. # version : Schema/wire version. Must be 0 (legacy) or 1 (this spec). # # The payload is appended to the recipient's FIFO queue. Returns immediately; # the recipient retrieves it via `fetch`. enqueue @0 (recipientKey :Data, payload :Data, channelId :Data, version :UInt16) -> (); # Fetch and atomically drain all queued payloads for a given recipient. # # recipientKey : Ed25519 public key of the caller (exactly 32 bytes). # channelId : Optional channel identifier (empty for legacy). # version : Schema/wire version. Must be 0 (legacy) or 1 (this spec). # # Returns the complete queue in FIFO order and clears it. Returns an empty # list if there are no pending messages. fetch @1 (recipientKey :Data, channelId :Data, version :UInt16) -> (payloads :List(Data)); } ``` --- ## Method-by-method analysis ### `enqueue @0` ``` enqueue (recipientKey :Data, payload :Data, channelId :Data, version :UInt16) -> () ``` **Purpose:** Append an opaque payload to a recipient's delivery queue. The DS stores the payload until the recipient fetches it. The call returns immediately after the payload is enqueued; it does not block until delivery. **Parameters:** | Parameter | Type | Size | Description | |---|---|---|---| | `recipientKey` | `Data` | Exactly 32 bytes | Ed25519 public key of the intended recipient. Used as the primary queue index. | | `payload` | `Data` | Variable (bounded by transport max) | Opaque byte string. Typically a TLS-encoded `MlsMessageOut` blob, but the DS does not inspect it. | | `channelId` | `Data` | 0 bytes (legacy) or 16 bytes (UUID) | Channel identifier for channel-aware routing. Empty `Data` is treated as the legacy default channel. | | `version` | `UInt16` | 2 bytes | Schema/wire version. `0` = legacy (no channel routing), `1` = current spec (channel-aware). | **Return value:** Void. The method returns `()` on success. Errors are surfaced as Cap'n Proto RPC exceptions. **Queue semantics:** Payloads are appended in FIFO order. The DS does not deduplicate, reorder, or inspect payloads. Multiple enqueue calls for the same recipient and channel ID are simply appended to the queue in the order they arrive. ### `fetch @1` ``` fetch (recipientKey :Data, channelId :Data, version :UInt16) -> (payloads :List(Data)) ``` **Purpose:** Fetch and atomically drain all queued payloads for a given recipient on a given channel. This is the "pull" side of the store-and-forward relay. **Parameters:** | Parameter | Type | Size | Description | |---|---|---|---| | `recipientKey` | `Data` | Exactly 32 bytes | Ed25519 public key of the caller. Must match the key used in the enqueue calls. | | `channelId` | `Data` | 0 bytes (legacy) or 16 bytes (UUID) | Channel identifier. Must match the `channelId` used during enqueue. | | `version` | `UInt16` | 2 bytes | Schema/wire version. Must match the version used during enqueue. | **Return value:** | Field | Type | Description | |---|---|---| | `payloads` | `List(Data)` | All queued payloads in FIFO order. Empty list if no messages are pending. | **Atomic drain:** The fetch operation returns the entire queue and clears it in a single atomic operation. There is no "peek" or partial fetch. This simplifies the concurrency model: the client processes all returned payloads and does not need to track which ones it has already seen. --- ## Channel-aware routing The `channelId` field enables per-channel queue separation. Each unique `(recipientKey, channelId)` pair maps to an independent FIFO queue on the server. ### Compound key structure ```text Queue Key = recipientKey (32 bytes) || channelId (0 or 16 bytes) ``` When `channelId` is empty (0 bytes), the queue key degenerates to just the `recipientKey`, preserving backward compatibility with legacy clients that do not use channels. ### Channel ID format The recommended format for `channelId` is a 16-byte UUID (128-bit, typically UUID v4). The DS treats the channel ID as an opaque byte string and does not parse its structure. Using UUIDs provides: 1. **Collision resistance** -- 2^122 random bits (for UUID v4) makes accidental collision negligible. 2. **Privacy** -- The channel ID reveals no information about the channel's participants or purpose. 3. **Fixed size** -- 16 bytes is compact and predictable for indexing. ### Use cases | Scenario | channelId | recipientKey | Result | |---|---|---|---| | Legacy client, no channels | Empty (0 bytes) | Alice's Ed25519 key | Single queue for all of Alice's messages | | 1:1 channel between Alice and Bob | UUID of the 1:1 channel | Alice's Ed25519 key | Separate queue for this specific channel | | Group channel | UUID of the group channel | Alice's Ed25519 key | Separate queue for this group's messages to Alice | --- ## Version field The `version` field provides a mechanism for wire-level schema evolution without breaking existing clients. | Version | Semantics | |---|---| | `0` | Legacy mode. `channelId` is ignored (treated as empty). Behaves like the pre-channel DeliveryService. | | `1` | Current specification. `channelId` is used for channel-aware routing. | The server validates the version field and rejects unknown versions as protocol errors. Clients must set the version field to match the schema revision they implement. --- ## FIFO queue semantics The Delivery Service provides strict FIFO ordering within each `(recipientKey, channelId)` queue: 1. **Enqueue order is preserved.** Payloads are returned by `fetch` in the exact order they were enqueued. 2. **Atomic drain.** Each `fetch` call returns all pending payloads and clears the queue. There is no risk of partial reads or interleaving. 3. **No persistence guarantees (current implementation).** The in-memory queue is lost on server restart. Persistent storage is planned for a future milestone. 4. **No redelivery.** Once a payload is returned by `fetch`, it is permanently removed. If the client crashes before processing it, the payload is lost. Reliable delivery with acknowledgments is a future enhancement. --- ## MLS-unaware design The DS intentionally does not parse, validate, or inspect MLS messages. All payloads are opaque `Data` blobs. This design has several consequences: - **Security:** The server cannot extract plaintext from MLS ciphertext, even if compromised. - **Simplicity:** The DS has no dependency on openmls or any MLS library. - **Flexibility:** The same DS can carry non-MLS payloads (e.g., signaling, metadata) without modification. - **No server-side optimization:** The DS cannot optimize delivery based on MLS message type (e.g., fanning out a Commit to all group members). The client must enqueue separately for each recipient. For the full design rationale, see [ADR-004: MLS-Unaware Delivery Service](../design-rationale/adr-004-mls-unaware-ds.md). --- ## Relationship to NodeService In the current unified architecture, the Delivery Service methods are exposed as part of the [NodeService interface](node-service-schema.md) with additional methods: | DeliveryService Method | NodeService Method | Additional Parameters | |---|---|---| | `enqueue @0` | `enqueue @2` | `auth :Auth` | | `fetch @1` | `fetch @3` | `auth :Auth` | | *(none)* | `fetchWait @4` | `auth :Auth`, `timeoutMs :UInt64` | The `fetchWait` method is a NodeService extension that provides long-polling semantics: it blocks until either new payloads arrive or the timeout expires. This avoids the latency and bandwidth overhead of repeated `fetch` polling. --- ## Further reading - [Wire Format Overview](overview.md) -- serialisation pipeline context - [NodeService Schema](node-service-schema.md) -- unified interface that subsumes DeliveryService - [Auth Schema](auth-schema.md) -- the companion service for KeyPackage management - [Envelope Schema](envelope-schema.md) -- legacy framing that used `mlsWelcome`/`mlsCommit`/`mlsApplication` message types - [ADR-004: MLS-Unaware Delivery Service](../design-rationale/adr-004-mls-unaware-ds.md) -- why the DS does not inspect MLS content