# Architecture ## High-level layout ``` ┌─────────────────────────────────────────────────────┐ │ VNOX Client │ │ (Rust + egui + wgpu) │ └────────────────────┬────────────────────────────────┘ │ LNEx v1 ┌──────────┴──────────┐ │ TCP │ UDP ▼ ▼ ┌─────────────────┐ ┌──────────────────┐ │ Gateway │ │ Voice Node │ │ (Rust/Tokio) │ │ (Rust) │ │ │ │ │ │ auth │ │ Opus encode │ │ channels │ │ UDP relay │ │ sessions │ │ jitter buffer │ │ permissions │ │ packet seq │ │ federation │ │ audio routing │ └────────┬────────┘ └──────────────────┘ │ │ LNEx Federation (Phase 3) ▼ ┌─────────────────┐ │ Other nodes │ │ (federated) │ └─────────────────┘ ``` --- ## Hybrid networking model VNOX uses two transports simultaneously. They serve different purposes and are never mixed. ### TCP — control plane Used for everything that requires ordering and reliability: - authentication and session setup - channel join / leave events - text chat messages - permission checks - federation routing signals ### UDP — data plane Used for everything where latency matters more than reliability: - voice packets (Opus frames) - realtime presence state Voice packets that are lost are not retransmitted. Loss is handled by Opus packet loss concealment and the jitter buffer. ### LNEx layer LNEx sits above both transports. It defines: - packet format and framing - compression - encryption (ChaCha20, see `02-protocol/security.md`) - routing between nodes - federation identity resolution LNEx is not tied to a specific transport. The same packet schema is used over both TCP and UDP, with transport-appropriate flags. --- ## Modules ### gateway Language: Rust Runtime: Tokio (async) Responsibilities: - client authentication via identity keypair - channel and session management - permission enforcement - federation routing (Phase 3) - text chat delivery Entry point for all TCP connections from clients. ### voice-node Language: Rust Responsibilities: - receive UDP voice packets from clients - decode Opus frames - apply jitter buffer - relay to other clients in the same channel - handle packet sequencing and reordering Can run on the same machine as the gateway or separately. ### client Language: Rust UI: egui Renderer: wgpu Audio: cpal + rodio + opus Networking: tokio The desktop application. Connects to a gateway via TCP (LNEx) and to a voice-node via UDP (LNEx). Handles all UI, audio capture, Opus encoding, and local identity management. Web client: not planned. Native only. ### overlay Language: Rust (separate process or injected) Status: Phase 2 In-game HUD showing: - who is speaking - current channel - latency - hotkey state ### plugins Language: TypeScript / JavaScript Runtime: Deno or QuickJS (not finalized) API: WebSocket RPC Sandboxed plugin environment. Plugins cannot access internals directly — only through the documented RPC API. ### federation (Phase 3) Handles cross-node communication: - node discovery - identity bridging - voice relay across nodes - channel bridging --- ## Tech stack summary | Component | Language | Key deps | |-----------|----------|----------| | Gateway | Rust | Tokio, serde, sqlx | | Voice node | Rust | opus, tokio | | Client | Rust | egui, wgpu, cpal, tokio | | Overlay | Rust | TBD | | Plugins | TypeScript | Deno / QuickJS | ### Why Rust - async performance via Tokio: handles thousands of concurrent connections - memory safety without GC: no pauses during voice transmission - strong UDP networking ecosystem - cross-platform native binary: Windows, macOS, Linux from one codebase - no Electron: the client is a real native application ### Serialization JSON for Phase 1 LNEx packets — simple, debuggable, no build-time codegen. Protobuf schemas live in `protocol/` and will replace JSON in Phase 2 without changing the wire framing (header stays identical). --- ## File structure ``` vnox/ ├── client/ # Desktop client (Rust + egui) ├── gateway/ # TCP gateway (Rust + Tokio) ├── voice-node/ # UDP voice relay (Rust) ├── federation/ # Federation layer (Phase 3) ├── protocol/ # LNEx .proto schemas + spec ├── plugins/ # Plugin runtime + example plugins ├── sdk/ # Client SDK └── docs/ # This documentation ``` --- ## Source layout conventions ### Rules - **≤ 200 lines per file.** If a file grows past that, split it. - **≤ 6 files per directory.** If a directory has more, introduce a subdirectory. - **KISS / DRY / SOLID.** One file = one clear responsibility. - Subdirectory always has a `mod.rs` that re-exports the public surface. Internal files are `pub(super)` or `pub(crate)` — never `pub` unless they are part of the module's public API. ### gateway/src ``` gateway/src/ ├── main.rs # startup: config, storage, TCP listener, spawn tasks ├── domain/ # pure business logic, no I/O │ ├── mod.rs │ ├── auth.rs # Ed25519 verify, challenge generation │ ├── channels.rs # ChannelStore: join/leave/members │ ├── config.rs # Config structs + load() │ ├── session.rs # SessionStore: create/get/remove │ └── storage.rs # SQLite via sqlx: messages, users, bans ├── net/ # network I/O │ ├── mod.rs │ ├── handshake.rs # HELLO → AUTH → SESSION exchange │ ├── io.rs # send_packet / read_packet / send_error │ └── state.rs # State (shared Arc clone) + BroadcastMsg ├── proto/ # wire protocol │ ├── mod.rs # re-exports everything │ ├── packet.rs # PacketId, ErrorCode, PacketHeader │ ├── payloads.rs # all JSON payload structs │ └── framing.rs # encode_packet, to_payload └── handler/ # per-connection request handling ├── mod.rs # run_session loop + dispatch + deliver ├── channel.rs # JOIN_CHANNEL / LEAVE_CHANNEL / broadcast_leave └── chat.rs # CHAT_MESSAGE persist + broadcast ``` **Dependency direction:** `main` → `net` → `domain` + `proto`. `handler` → `net` + `domain` + `proto`. No cycles. ### voice-node/src ``` voice-node/src/ ├── main.rs # config, UdpSocket::bind, recv loop ├── relay.rs # relay_packet, add_member, remove_member └── jitter.rs # JitterBuffer (reorder by voice_seq) ``` Small crate — no subdirectories needed yet. ### client/src ``` client/src/ ├── main.rs # tokio runtime, identity load, eframe::run_native ├── identity.rs # keypair gen/load, Identity struct ├── net/ # LNEx TCP + UDP networking │ ├── mod.rs # NetHandle, spawn(), session_loop │ ├── types.rs # NetCommand, NetEvent, MemberInfo, ChatMsg │ ├── wire.rs # PID_* constants + all wire payload structs │ ├── framing.rs # read() / write() raw packets over TcpStream │ ├── handshake.rs # HELLO → AUTH → SESSION │ ├── dispatch.rs # incoming packet → NetEvent mapping │ └── voice.rs # build_packet(), spawn_recv() UDP loop ├── audio/ # cpal + Opus pipeline │ ├── mod.rs # start(), AudioPipeline, EncodedFrame, DecodedFrame │ ├── config.rs # find() best StreamConfig for device │ ├── capture.rs # cpal input → Opus encode → EncodedFrame channel │ └── playback.rs # Opus decode → cpal output └── ui/ # egui layout ├── mod.rs # VnoxApp, eframe::App impl, poll_net() ├── state.rs # UiState, ConnState, Channel, ChatMessage ├── theme.rs # color constants (BG_BASE, ACCENT, …) ├── widgets.rs # channel_row(), message_row(), fmt_ts() ├── sidebar.rs # node switcher strip + channel list panel ├── chat.rs # chat area: header, scroll, input box └── connect.rs # connect screen (shown when disconnected) ``` **Dependency direction:** `main` → `net` + `audio` + `ui`. `ui` → `net::types` (read-only). `audio` is standalone. `net` → `identity`. No cycles. ### Adding a new feature — checklist 1. Decide which crate owns it (`gateway`, `voice-node`, `client`). 2. Decide which layer it belongs to: - pure logic with no I/O → `domain/` - network I/O → `net/` - UI rendering → `ui/` - audio → `audio/` 3. Create a new file in the right subdirectory. 4. If the subdirectory now has > 6 files, split into a deeper level. 5. Re-export from `mod.rs` only what callers actually need. 6. Keep the file under 200 lines. If it grows, extract a helper module. 7. Run `cargo check` before committing. ### What goes where — quick reference | Thing | File | |-------|------| | New packet type | `gateway/src/proto/payloads.rs` + `client/src/net/wire.rs` | | New gateway handler | `gateway/src/handler/` (new file if > 200 lines) | | New domain rule (ban, rate limit…) | `gateway/src/domain/` | | New UI panel | `client/src/ui/` (new file) | | New audio processing step | `client/src/audio/` (new file) | | Config field | `gateway/src/domain/config.rs` | | DB schema change | `gateway/src/domain/storage.rs` → `migrate()` |