# OwnCord Client (Tauri v2) TypeScript frontend (Vite, vanilla TS — no React/Vue) plus a deliberately thin Rust backend in `src-tauri/` for native APIs only. LiveKit handles voice/video. ## Layout - `src/stores/` observable stores · `src/lib/` protocol, WS, voice, E2EE · `src/pages/`, `src/components/` UI - `src/lib/protocolTypes.ts` and `src/generated/` are generated — see the root CLAUDE.md - `tests/unit`, `tests/integration`, `tests/contract` (vitest, jsdom) · `tests/e2e`, `tests/e2e/admin`, `tests/e2e/native` (Playwright) · `tests/browser` (vitest browser mode) - A test whose assertions read, import or execute a **`Server/`-owned** artifact belongs in `tests/contract`, not `tests/unit` — `src-tauri/` is part of this component, so reading it is an ordinary unit test. The rule is in [docs/contributing.md](../docs/contributing.md#testing) - `src/platform/` does **not** exist yet. Where the desktop/browser seam will go, and which 20 files hold the native imports that must move behind it, is recorded in [docs/architecture/platform-contracts.md](../docs/architecture/platform-contracts.md). Building it is B7 — do not start it as a side effect of another change. ## Gotchas - Node's native Web Storage (Node 22+) shadows jsdom's `localStorage`; `tests/setup.ts` replaces it with an in-memory shim, so the suite runs on modern Node without `--no-experimental-webstorage`. If storage tests fail en masse, suspect that shim before your change. CI pins Node 24. - `src/lib/dispatcher.ts` is the single WS-event entry point **into the stores**: server events reach domain stores only through a `ws.on(...)` subscription registered there. Other modules do register their own `ws.on(...)` handlers for page-local UI (`main.ts`, `MainPage.ts`, `ChannelController.ts` — ringing, overlays, slow-mode timers); that is fine as long as they only _read_ store state. Writing a store from one of those handlers is the violation, and `local/no-store-write-in-ws-on` now fails the build on it. - Voice sessions are superseded, not cancelled. `LiveKitSession` re-entry points check whether a newer attempt owns the shared state before tearing anything down, so cleanup in an aborted path must be scoped to that attempt's own room — a global `leaveVoice()` there kills the live session. - Voice E2EE is key-holder based with TOFU identity pinning. Anything touching `livekitE2EE.ts` or `identity.ts` must preserve the epoch/keypair staleness guards and must never report an unverified peer as verified. - Do not run `npm run tauri build` locally; the desktop build is CI-only. - Formatting is prettier-enforced; match the surrounding code rather than reasoning about style.