# 1:1 Channel Design

This page describes the design for first-class 1:1 (direct message) channels in
quicproquo. 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
