Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
6.3 KiB
Design Decisions Overview
This section collects the Architecture Decision Records (ADRs) that document the key design choices in quicprochat. Each ADR follows a standard format: context (why the decision was needed), decision (what was chosen), and consequences (trade-offs, benefits, and residual risks).
These decisions are not immutable. Each ADR has a status field and can be superseded by a later ADR if circumstances change. The goal is to preserve the reasoning behind each choice so that future contributors understand why the system works the way it does, not just how.
ADR index
| ADR | Title | Status | One-line summary |
|---|---|---|---|
| ADR-002 | Cap'n Proto over MessagePack (v1) | Superseded | Zero-copy, schema-enforced serialisation with built-in async RPC replaced hand-rolled MessagePack dispatch. Superseded by ADR-007. |
| ADR-004 | MLS-Unaware Delivery Service | Accepted | The DS routes opaque blobs by recipient key; it never inspects MLS content. |
| ADR-005 | Single-Use KeyPackages | Accepted | The AS atomically removes a KeyPackage on fetch to preserve MLS forward secrecy. |
| ADR-007 | v1 Cap'n Proto to v2 Protobuf Migration | Accepted | Replace Cap'n Proto RPC with custom Protobuf framing over QUIC streams for better ecosystem support, 44-method surface, and multi-threaded dispatch. |
Design comparison
For a broader comparison of quicprochat's design against alternative messaging protocols (Signal, Matrix/Olm/Megolm), see Why This Design, Not Signal/Matrix/....
How to read an ADR
Each ADR page follows this structure:
- Status -- One of: Proposed, Accepted, Deprecated, Superseded. All current ADRs are Accepted unless noted.
- Context -- The problem or force that motivated the decision. What constraints existed? What alternatives were considered?
- Decision -- The specific choice that was made. What was selected and what was rejected?
- Consequences -- The trade-offs that result from the decision. What are the benefits? What are the costs? What residual risks remain?
- Code references -- Links to the source files where the decision is implemented.
ADR-007: v1 Cap'n Proto to v2 Protobuf Migration
Status: Accepted
Context
quicprochat v1 used Cap'n Proto for both serialisation and RPC dispatch via
capnp-rpc. This worked well for the initial 8-method NodeService interface
but had several limitations as the protocol expanded:
!Sendconstraint:capnp-rpcusesRc<RefCell<>>internally, requiring all RPC futures to run on atokio::task::LocalSet. This prevented multi-threaded dispatch and added complexity to every connection handler.- Schema growth friction: Cap'n Proto's capability-based RPC model does not map cleanly to large flat method tables. Adding the 36 new methods (keys, blob, device, federation, moderation, recovery, etc.) would have required significant schema refactoring.
- ALPN collision: The
b"capnp"ALPN identifier is not registered and could conflict with other Cap'n Proto deployments. A project-specific ALPN is cleaner. - Tooling:
capnpcrequires a system-wide binary installation or a vendored copy.prost-buildwithprotobuf-srcself-vendorsprotoc, eliminating the build-time dependency.
Decision
Replace capnp-rpc with a custom binary framing layer (quicprochat-rpc) and
Protocol Buffers (prost) for payload serialisation:
- Three frame types: Request (10-byte header), Response (9-byte header), Push (6-byte header), all carrying Protobuf-encoded payloads.
- Method IDs are numeric
u16constants dispatched via a handler registry. One QUIC bidirectional stream per RPC call; push events on QUIC uni-streams. - ALPN changed from
b"capnp"tob"qpc". Default port changed from 7000 to 5001. - Cap'n Proto legacy types are retained in
quicprochat-protofor v1 compatibility but are no longer used for RPC dispatch.
Consequences
Benefits:
- Full
tokio::spawnconcurrency (noLocalSetrequired). - 44-method RPC surface with clean numeric namespace and room to grow.
- Self-contained build (no system
protocdependency). - Lighter middleware integration via Tower
Servicetraits. - Push event delivery without polling.
Costs:
- Lost Cap'n Proto zero-copy reads (Protobuf requires deserialisation). Acceptable
because the hot path in the Delivery Service works with opaque
bytes::Byteswithout deserialisation. - Lost promise pipelining from
capnp-rpc. Not required for the current RPC surface; can be re-added with a future streaming RPC design. - v1 clients are no longer wire-compatible with v2 servers.
Code references
- Frame format:
crates/quicprochat-rpc/src/framing.rs - Method IDs:
crates/quicprochat-proto/src/lib.rs(method_idsmodule) - Proto schemas:
proto/qpc/v1/*.proto
Cross-cutting themes
Layered security
The core principle is that no single layer is trusted alone. QUIC/TLS transport encryption protects metadata and provides server authentication; MLS provides end-to-end content encryption with forward secrecy and post-compromise security; OPAQUE ensures the server never learns the user's password.
Server minimalism
ADR-004 and ADR-005 reflect a design philosophy where the server does as little as possible. The DS does not parse MLS messages. The AS enforces single-use semantics through atomic removal rather than complex state tracking. This minimalism reduces the server's attack surface and makes it easier to audit.
Schema-first design
The v2 protocol defines all messages and method IDs in checked-in source files
(proto/qpc/v1/*.proto and crates/quicprochat-proto/src/lib.rs). Every wire
type is documented, versioned, and evolvable through the standard Protobuf
schema evolution rules (adding optional fields, reserving removed field numbers).
Further reading
- Why This Design, Not Signal/Matrix/... -- comparative analysis against alternative protocols
- Wire Format Reference -- the serialisation pipeline that implements these decisions
- Architecture Overview -- system-level view
- Protocol Layers Overview -- how the protocol layers stack