---
id: PRD-0003
title: "M2 — Central Server (serve)"
status: done
owner: "Aaron Roney"
created: 2026-07-01
updated: 2026-07-01

depends_on:
- PRD-0002

principles:
- "Bridges dial out; the server never dials in (DESIGN §11). One outbound WSS per (session, server)."
- "Presence == a live, heartbeat-confirmed connection; nothing is stored, nothing is queued (DESIGN §9/§10)."
- "Admin is a user role from the serve-config allowlist; any of that user's machines/sessions may administer (DESIGN §7)."

references:
- name: "Conclave design (server §13, admin §7, presence §10, transport §11, channels §6, data flow §14)"
  url: "docs/DESIGN.md"
- name: "razel per-connection tracing spans + integration-test SOC"
  url: "https://github.com/twitchax/razel"

acceptance_tests:
- id: uat-001
  name: "Register claims a username + enrolls machine #1; duplicate username and duplicate live handle are rejected"
  command: cargo nextest run -E 'test(/server_register/)'
  uat_status: verified
- id: uat-002
  name: "Challenge-response auth resolves a machine key to (user, machine); bad/revoked key is refused"
  command: cargo nextest run -E 'test(/server_auth/)'
  uat_status: verified
- id: uat-003
  name: "Channel create + user-level ACL + invite redeem (adds username to ACL); private names never leak to non-members"
  command: cargo nextest run -E 'test(/server_channel/)'
  uat_status: verified
- id: uat-004
  name: "Fan-out: channel message reaches all subscribers; whisper reaches exactly one session path"
  command: cargo nextest run -E 'test(/server_fanout/)'
  uat_status: verified
- id: uat-005
  name: "Presence heartbeat reaps a half-open connection; revocation (machine remove / kick / ban) force-drops live sessions"
  command: cargo nextest run -E 'test(/server_presence/)'
  uat_status: verified

tasks:
- id: T-001
  title: "axum WSS endpoint + connection lifecycle"
  priority: 1
  status: done
  notes: "Accept loop, per-connection info_span!(\"conn\", id=…), Hello/version-negotiate, then challenge-response auth. Load serve config (bind addr, TLS/rustls or behind cloudflared, data-dir, admins allowlist)."
- id: T-002
  title: "Registration + machine enrollment/revocation"
  priority: 1
  status: done
  notes: "register (claim username + enroll machine #1, self-authorizing); machine add/remove authed as the user; revocation force-drops the affected live sessions immediately."
- id: T-003
  title: "Challenge-response auth + handle uniqueness"
  priority: 1
  status: done
  notes: "Server-issued nonce, Ed25519 verify -> (user, machine). New machine proves possession on first connect. Enforce unique live handle per (user, machine) — reject or auto-suffix."
- id: T-004
  title: "Channels: visibility, user-level ACL, invite tokens"
  priority: 1
  status: done
  notes: "create/rename/set-visibility/delete (channel-admin=created_by). ACL of usernames. Invite mint + redeem (redeem adds username to ACL). Discovery list_channels visibility-gated; who membership-gated; never leak private/unlisted names to non-members."
- id: T-005
  title: "Admin authorization"
  priority: 2
  status: done
  notes: "Server admins from config allowlist (server-wide); channel admins = created_by. Authorize admin RPCs by role; non-admin -> AclError/authz error."
- id: T-006
  title: "Presence + heartbeat reaping"
  priority: 1
  status: done
  notes: "In-memory presence keyed by full path. WS ping/pong + idle timeout reaper for half-open connections. who query. No store-and-forward."
- id: T-007
  title: "Fan-out router"
  priority: 1
  status: done
  notes: "Channel message -> all subscribed sessions; whisper -> exactly one session path (error if offline/unknown). At-most-once, best-effort, no acks/dedup."
- id: T-008
  title: "Server-side integration test harness"
  priority: 2
  status: done
  notes: "Spin up serve in-process; drive multiple simulated bridge clients over in-memory or loopback transport. Cover all UAT scenarios. Use .config/nextest.toml network-heavy group for socket-based cases."
---

# Summary

The central relay: an axum WebSocket-over-TLS endpoint that authenticates machines, manages users
/machines/channels/ACLs/invites, tracks live presence with heartbeats, authorizes admin actions,
and fans out channel messages and whispers.

# Problem

This is the hub every participant connects to — without it there is no fabric. It owns identity
resolution, membership/authorization, presence truth, and routing.

# Goals

1. Authenticated, versioned WSS sessions (register + challenge-response over the M1 protocol).
2. Channels with visibility tiers, user-level ACLs, and invite-token membership.
3. Accurate presence (heartbeat-reaped) + fan-out for channel messages and one-session whispers.
4. Role-based admin authorization from the serve-config allowlist, with revocation force-drop.

# Technical Approach

`conclave serve` runs an axum WSS server. Each accepted connection gets a tracing span, negotiates
protocol version, then authenticates via challenge-response (M1 identity). Durable state (users,
machines, channels, invites) lives in the embedded SurrealDB repo (M1); presence + subscriptions
are in-memory maps keyed by full participant path. A ping/pong heartbeat reaps zombies. The fan-out
router dispatches channel messages to subscribers and whispers to a single session. Admin RPCs are
authorized against the config allowlist (server admins) or channel `created_by` (channel admins).

# Assumptions

- M1 protocol, identity, and store are complete and tested.
- TLS is provided by rustls directly or terminated at cloudflared (loopback origin).

# Constraints

- Single instance in v1 (presence in-process; no cross-instance fan-out) — DESIGN §19 for HA.
- No message persistence or store-and-forward.

# References to Code

- `docs/DESIGN.md` §6 (channels), §7 (admin), §9/§10 (presence), §11 (transport), §13/§14.
- razel: per-connection spans, `tests/*_integration.rs` bounded-subsystem pattern.

# Non-Goals (MVP)

- The bridge / MCP integration (M3) and the CLI/skill surface (M4).
- Rate limiting / flood control (DESIGN §19).

# History
(Entries appended during implementation go below this line.)

## 2026-07-01 — M2 executed (T-001…T-008)

The central server landed with TDD; `cargo make ci` is green (73 tests). All five UATs pass:
`test(/server_register/)` (3), `test(/server_auth/)` (4), `test(/server_channel/)` (3),
`test(/server_fanout/)` (2), `test(/server_presence/)` (4), plus one WebSocket e2e.

Structure — the `server` module is split by responsibility behind a thin public surface
(`serve`, `ServerConfig`, `AclError`):

- **`server::hub`** — the transport-agnostic core. Durable identity/channel state in the M1
  `Store`; live presence, channel subscriptions, and channel bans are in-memory maps keyed by full
  `SessionPath` behind one `std::sync::Mutex` (short, await-free critical sections). Each live
  session holds an unbounded outbound frame channel + a kill `Notify`; the fan-out router pushes
  channel messages (sender excluded) and whispers by cloning those senders. Every hub method is
  transport-free and unit-testable — it returns the response frame or a `ProtocolError`.
- **`server::session`** — the per-connection handshake state machine + inbound frame loop, driven
  over plain `mpsc` channels so it is transport-agnostic. `Hello`→version-negotiate→`Challenge`→
  (`Register`+`Auth` for a new user, or `Auth` for an enrolled machine)→`Established`. The
  client-supplied `from` is ignored — the server stamps the authenticated path (anti-forgery, §12).
- **`server::wss`** — the axum WebSocket adapter (one binary message per frame) + the `serve`
  entrypoint + the background heartbeat reaper.

Locked-decision notes and deviations (none change the design intent):

- **Transport: plain-HTTP axum origin, not rustls.** TLS terminates at cloudflared and the origin
  hop is loopback (§11/§12), so M2 ships plain WS; rustls stays a future toggle. The byte-stream
  (length-delimited) transport used by the integration harness is `#[cfg(test)]`-only.
- **Heartbeat is an app-level `Ping`/`Pong` frame**, not WS-native ping/pong — uniform across the
  duplex (test) and WS (prod) transports. The reaper drops any session with no inbound frame within
  a 60s idle timeout; deterministic reaper tests use tokio's paused clock (`start_paused`).
- **Additive wire frames (forward-compat preserved, all appended):** `ListChannels`, `ChannelList`,
  `Joined`, `Ack`, `InviteToken`, `Ping`, `Pong` on `ProtocolMessage`; `MachineAdd` on `AdminOp`
  (self-service enrollment); a `ChannelInfo` struct. Existing variant indices are unchanged (a test
  asserts `Hello` still encodes at discriminant 0), so the protocol version stays 1.
- **Revocation semantics:** `machine remove` force-drops (disconnects) every live session for that
  `(user, machine)`; `kick`/`ban` drop the target from the *channel* (unsubscribe) but leave the
  connection up for its other channels. Channel bans are in-memory in v1 (reset on restart).
- **Admin authorization:** any authenticated user may create a channel (becoming its
  `created_by`/channel-admin); channel ops require channel-admin or a server-admin from the
  `--admin` allowlist; `user remove` is server-admin-only; `machine add/remove` are self-service.
- New deps: `axum` (ws), `futures-util` (WS split), `tokio` `sync`/`time`/`net`/`signal`;
  dev-only `tokio-tungstenite` (e2e client) and `tokio` `test-util` (paused clock). The `bridge`
  module (M3) remains a `//!` stub.
