Files
quicproquo/docs/src/roadmap/dm-channels.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
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
2026-03-21 19:14:06 +01:00

262 lines
9.1 KiB
Markdown

# 1:1 Channel Design
This page describes the design for first-class 1:1 (direct message) channels in
quicprochat. Channels provide per-conversation authorisation, MLS-encrypted
payloads, message retention with TTL eviction, and backward compatibility with
the legacy delivery model.
For the broader roadmap context, see [Milestones](milestones.md) and
[Production Readiness WBS](production-readiness.md) (Phase 4).
---
## Goals
1. **First-class 1:1 channels.** Each conversation between two participants has
a unique `channelId`, enabling per-channel authorisation, storage, and
eviction.
2. **Per-channel authorisation.** The server enforces that only the two channel
members can enqueue and fetch messages for a given channel.
3. **MLS-encrypted payloads.** All message content is MLS ciphertext. The server
never sees plaintext. Channel metadata (ID + participant keys) is the only
information the server holds.
4. **7-day message retention.** Messages older than 7 days are evicted. This is
configurable but defaults to 7 days.
5. **24-hour KeyPackage TTL.** KeyPackages expire after 24 hours. Clients must
rotate KeyPackages before expiry to remain reachable.
---
## Schema Changes (Cap'n Proto)
### New Fields
The following fields are added to the existing `NodeService` RPC methods:
| RPC Method | New Field | Type | Description |
|------------|-----------|------|-------------|
| `enqueue` | `channelId` | `Data` (UUID, 16 bytes) | Target channel |
| `fetch` | `channelId` | `Data` (UUID, 16 bytes) | Channel to fetch from |
| `fetchWait` | `channelId` | `Data` (UUID, 16 bytes) | Channel to long-poll |
| All messages | `version` | `UInt16` | Wire version for forward compat |
### Version Field
The `version` field on delivery messages allows the server to reject messages
with unknown versions. The current version is `1`. Clients that do not set
`channelId` are treated as version `0` (legacy mode).
### New RPC Method
A new `createChannel` method is added to `NodeService`:
```capnp
createChannel @N (
auth :Auth,
peerKey :Data # Ed25519 public key of the other participant
) -> (
channelId :Data # UUID, 16 bytes
);
```
The server generates the `channelId`, stores the membership, and returns the ID
to the caller. The peer discovers the channel when they receive a message
addressed to it (or via a separate discovery mechanism in a future milestone).
---
## AuthZ Model
### Channel Membership
Each channel has exactly two members, identified by their Ed25519 public keys:
```
Channel {
channelId: UUID (16 bytes)
members: {a_key: Ed25519PubKey, b_key: Ed25519PubKey}
created_at: Timestamp
}
```
The server stores this mapping and enforces it on every operation.
### Enqueue Authorisation
When a client calls `enqueue(auth, channelId, recipientKey, payload)`:
1. Validate the `Auth` token (see [Auth, Devices, and Tokens](authz-plan.md)).
2. Look up the channel by `channelId`.
3. Verify that the caller's identity (from the token) is one of the channel's
two members.
4. Verify that `recipientKey` is the *other* member of the channel (prevents
sending to yourself or to a non-member).
5. Apply rate limits (50 r/s per identity, 5 MB payload cap).
6. Enqueue the payload.
### Fetch Authorisation
When a client calls `fetch(auth, channelId, recipientKey)` or
`fetchWait(auth, channelId, recipientKey, timeout)`:
1. Validate the `Auth` token.
2. Verify that the caller's identity matches `recipientKey`.
3. Verify that `recipientKey` is a member of the specified channel.
4. Return messages for `(channelId, recipientKey)`, filtering out expired
messages (TTL check).
---
## Storage Model
### Channels Table
| Column | Type | Description |
|--------|------|-------------|
| `channel_id` | UUID (16 bytes) | Primary key |
| `member_a_key` | Ed25519 public key (32 bytes) | First member |
| `member_b_key` | Ed25519 public key (32 bytes) | Second member |
| `created_at` | Timestamp | Channel creation time |
A unique constraint on `(member_a_key, member_b_key)` (sorted) prevents
duplicate channels between the same pair of identities.
### Delivery Queue
Messages are keyed by `(channelId, recipient_key)`:
| Column | Type | Description |
|--------|------|-------------|
| `channel_id` | UUID (16 bytes) | Channel |
| `recipient_key` | Ed25519 public key (32 bytes) | Intended recipient |
| `payload` | Bytes | MLS ciphertext (opaque to server) |
| `received_at` | Timestamp | Server receive time |
| `sequence_no` | UInt64 | Per-channel, per-recipient monotonic counter |
### TTL Eviction
Messages are evicted in two ways:
1. **Fetch-time check:** When a client fetches messages, the server filters out
any message where `received_at + TTL < now`. This is the primary eviction
path.
2. **Background sweep:** A periodic task (configurable interval, default 1 hour)
scans for and deletes expired messages. This prevents unbounded storage
growth from inactive channels.
Default TTL values:
| Entity | TTL | Configurable |
|--------|-----|-------------|
| Messages | 7 days | Yes |
| KeyPackages | 24 hours | Yes |
---
## Flows
### Create Channel
```
Alice Server Bob
| | |
|-- createChannel(auth, bob_key) | |
| |-- generate channelId |
| |-- store {channelId, |
| | alice_key, bob_key} |
|<- channelId ------------------| |
| | |
```
Alice receives the `channelId` and can now send messages to Bob on this channel.
Bob discovers the channel when he receives the first message (the `channelId` is
included in the delivery metadata).
### Send (with AuthZ)
```
Alice Server
| |
|-- enqueue(auth, channelId, |
| bob_key, mls_ciphertext) |
| |-- validate auth token
| |-- lookup channel membership
| |-- verify alice_key in members
| |-- verify bob_key is recipient
| |-- check rate limits
| |-- store (channelId, bob_key,
| | payload, received_at, seq)
|<- ok (sequence_no) ------------|
| |
```
### Receive (with TTL)
```
Bob Server
| |
|-- fetchWait(auth, channelId, |
| bob_key, timeout) |
| |-- validate auth token
| |-- verify bob_key in channel
| |-- query (channelId, bob_key)
| |-- filter: received_at + 7d > now
| |-- return non-expired messages
|<- messages[] ------------------|
| |
```
---
## Backward Compatibility
### Legacy Mode (channelId = nil)
When `channelId` is empty or absent:
- The server treats the request as a legacy delivery (pre-channel behavior).
- Messages are routed solely by `recipientKey`, without channel-level authz.
- This mode can be disabled in production via server configuration.
### Version Negotiation
The `version` field on delivery messages allows clean rejection of future schema
changes:
| Version | Behavior |
|---------|----------|
| 0 | Legacy mode: no `channelId`, no per-channel authz |
| 1 | Channel-aware: `channelId` required, authz enforced |
The server rejects messages with `version > max_supported`.
---
## Open Items
These items are deferred to future milestones:
- **Persistence backend:** The current `DashMap`-based store must be extended to
SQLite (or SQLCipher) for durable channel and delivery state. See
[Milestones: M6](milestones.md#m6----persistence-planned).
- **Channel discovery API:** A dedicated RPC for Bob to discover channels he is
a member of, rather than relying on first-message discovery.
- **Client UX:** Map peer identity to `channelId` discovery; cache `channelId`
in the client state file.
- **Audit logging:** Log channel creation, authz failures, send/recv events with
redaction of ciphertext. See [Auth, Devices, and Tokens](authz-plan.md) for
the audit logging design.
- **Multi-device:** A single account on multiple devices sharing the same
channel. Requires per-device delivery queues and MLS multi-device support.
---
## Cross-references
- [Milestones](milestones.md) -- M4 (CLI subcommands) and M6 (persistence)
- [Production Readiness WBS](production-readiness.md) -- Phase 4 (Delivery Semantics)
- [Auth, Devices, and Tokens](authz-plan.md) -- token validation and identity binding
- [Wire Format: Delivery Schema](../wire-format/delivery-schema.md) -- current delivery schema
- [Wire Format: NodeService Schema](../wire-format/node-service-schema.md) -- RPC interface
- [Architecture Overview](../architecture/overview.md) -- system diagram and service model