2026-07-19 12:18:20 +00:00
# Client Architecture (Tauri)
2026-08-07 21:20:48 +02:00
**Verified against:** commit `5630aa1` , 2026-08-04
2026-07-19 12:18:20 +00:00
2026-08-07 21:20:48 +02:00
Desktop client built on Tauri v2: a TypeScript webview (~42k LOC, vanilla TS —
no UI framework) plus ~4.7k LOC of Rust across 16 modules. State lives in a
hand-rolled reactive store (`src/lib/store.ts` : immutable updates,
microtask-batched notifications, selector subscriptions). Components are
factory functions returning `{ element, mount, destroy }` built with the
`@lib/dom` helpers; a 2-page state machine (`src/lib/router.ts` ) switches
between the Connect and Main pages.
2026-07-19 12:18:20 +00:00
2026-08-07 21:20:48 +02:00
> `docs/client-architecture.md` is a 15-line redirect stub kept for old links;
> this document is the client architecture reference. The abandoned SolidJS
> beachhead that used to live under `src/components/solid/` was fully removed
> (audit A-2026-07-12, closed 2026-07-19).
2026-07-19 12:18:20 +00:00
## D7 — Module map
```mermaid
flowchart TB
subgraph boot ["Bootstrap"]
MAIN["main.ts<br/>page orchestration, auth wiring,<br/>appearance pre-render"]
end
subgraph rust ["Rust (src-tauri)"]
2026-08-07 21:20:48 +02:00
TOFU["tofu.rs<br/>shared TOFU core:<br/>3 rustls verifiers + pure decide"]
WSP["ws_proxy.rs<br/>WSS proxy"]
HTP["http_proxy.rs<br/>loopback TCP→TLS REST tunnel"]
LKP["livekit_proxy.rs<br/>loopback TLS tunnel (pin required)"]
CRED["credentials.rs + secret_store.rs<br/>OS keychain + verified fallback"]
2026-07-19 12:18:20 +00:00
PTT["ptt.rs<br/>push-to-talk polling"]
UPDC["update_commands.rs<br/>self-hosted updater"]
2026-08-07 21:20:48 +02:00
SET["commands.rs<br/>settings + cert/identity pin stores"]
2026-07-19 12:18:20 +00:00
end
subgraph comm ["Communication layer (src/lib)"]
2026-08-07 21:20:48 +02:00
API["api.ts<br/>REST client via httpProxy.ts<br/>(TOFU-pinned Rust tunnel)"]
WSC["ws.ts<br/>reconnect w/ backoff, seq replay,<br/>generation counters, cert-tofu events"]
DISP["dispatcher.ts<br/>34 msg types → store mutators"]
LKS["livekitSession.ts (1.4k LOC)<br/>voice state machine"]
LKE["livekitE2EE.ts<br/>key-holder election, room-key<br/>wrap/unwrap, peer verification"]
2026-07-19 12:18:20 +00:00
end
2026-08-07 21:20:48 +02:00
subgraph state ["Stores (9 singletons)"]
2026-07-19 12:18:20 +00:00
AUTH2["auth"]
2026-08-07 21:20:48 +02:00
CHAN["channels<br/>(incl. roles)"]
2026-07-19 12:18:20 +00:00
MSG["messages"]
MEM["members"]
VOICE["voice"]
DM["dm"]
2026-08-07 21:20:48 +02:00
BLK["blocks"]
EMO["emoji"]
2026-07-19 12:18:20 +00:00
UIS["ui"]
end
subgraph ui ["UI (imperative DOM)"]
CP["ConnectPage<br/>profiles + health polling"]
2026-08-07 21:20:48 +02:00
MP["MainPage<br/>SidebarArea / ChatArea /<br/>15 controllers"]
COMP["~60 component files<br/>message-list, settings tabs,<br/>voice widgets, overlays"]
2026-07-19 12:18:20 +00:00
end
MAIN --> CP
MAIN --> MP
MAIN --> API
MAIN --> WSC
WSC --> WSP
2026-08-07 21:20:48 +02:00
API --> HTP
WSP --> TOFU
HTP --> TOFU
LKP --> TOFU
2026-07-19 12:18:20 +00:00
WSC --> DISP
2026-08-07 21:20:48 +02:00
DISP --> AUTH2 & CHAN & MSG & MEM & VOICE & DM & BLK & EMO & UIS
2026-07-19 12:18:20 +00:00
state --> ui
LKS --> LKP
2026-08-07 21:20:48 +02:00
LKS --> LKE
2026-07-19 12:18:20 +00:00
VOICE --> LKS
MP --> COMP
2026-08-07 21:20:48 +02:00
WSP --> SRV["Go server"]
HTP --> SRV
2026-07-19 12:18:20 +00:00
CRED -.-> MAIN
UPDC -.-> MAIN
%% cross-store coupling (audit finding)
AUTH2 -.->|clearAuth → leaveVoice| VOICE
VOICE -.-> MEM
```
**What this shows.** Data flows one way in the happy path: WS frame → Rust
`ws_proxy` → `ws.ts` → `dispatcher.ts` → store mutators → subscribed components
2026-08-07 21:20:48 +02:00
re-render. All three network paths — WebSocket, REST, and LiveKit — terminate
TLS inside Rust proxies that share one TOFU core (`tofu.rs` ): the WS and HTTP
proxies use a capture-then-decide verifier, and the LiveKit proxy refuses to
start without an existing pin. Deciding never writes a pin — a first
connection is *rejected* and surfaced to the user as a blocking trust prompt
before any pin is stored (the former auto-pin-on-first-use behavior was
removed in the 2026-07-22 security remediation). The remaining dashed edges
mark cross-store coupling (auth→voice→members) — known structural debt, not
yet scheduled.
2026-07-19 12:18:20 +00:00
### Key mechanisms
| Concern | Where | How |
|---------|-------|-----|
| Reconnect | `src/lib/ws.ts` | Exponential backoff (cap 30s), heartbeat 30s, `last_seq` replay + bounded dedup set, generation counter invalidates stale listeners |
2026-08-07 21:20:48 +02:00
| Cert trust | `src-tauri/src/tofu.rs` (shared by `ws_proxy.rs` , `http_proxy.rs` , `livekit_proxy.rs` ) | TOFU with explicit consent: fingerprints stored per host in `certs.json` , but *deciding never writes a pin* — first use and mismatch both reject the connection and emit a `cert-tofu` event; the TS side shows a blocking modal (`CertMismatchModal.ts` ) and only an explicit Accept stores/updates the pin. The updater uses a fourth, host-scoped verifier (pin for the OwnCord host, WebPKI for GitHub). |
| Voice E2EE identity | `src/lib/identity.ts` + `src-tauri/src/commands.rs` | Long-term ECDSA identity key in the OS keyring (`identity:{host}` ); peer identity keys pinned in `identity_pins.json` ; changed peer key → blocking identity-mismatch modal with safety-number comparison |
2026-07-19 12:18:20 +00:00
| Credentials | `src-tauri/src/credentials.rs` | OS keychain per host; password field `serde(skip)` so it never crosses IPC back to JS |
| Multi-server | `src/lib/profiles.ts` | Server profiles w/ 15s health polling and auto-connect; one active connection, quick-switch replaces WS + tunnels |
2026-07-20 14:51:23 +02:00
| HTTP capability | `src-tauri/capabilities/default.json` | `http:allow-fetch` is the only URL-scoped identifier (the other two `fetch_*` commands take a validated `ResourceId` ); allows `https://*` + `http://127.0.0.1:*` , denies https loopback. Wildcard is required by link previews — see [docs/plans/tauri-capability-narrowing.md ](../plans/tauri-capability-narrowing.md ) |
2026-07-19 12:18:20 +00:00
| Updates | `src/lib/updater.ts` + `update_commands.rs` | Endpoint derived from the connected server URL, https-only, TLS pinned to TOFU fingerprint, minisign-verified |
| Settings | `commands.rs` + `src/lib/preferences.ts` | Split persistence: Rust store (`settings.json` , key-allowlisted) *and* raw `localStorage` for UI prefs/themes |
| Theming | `src/lib/themes.ts` + `styles/tokens.css` | CSS custom properties; 4 built-in themes + custom overrides |
2026-07-20 13:29:57 +02:00
| GIF picker | `src/lib/gifProvider.ts` + `components/GifPicker.ts` | Calls the user's own server (`/api/v1/gif/*` ) through `api.ts` — no provider API key in the bundle. Server answers `503 GIF_DISABLED` when unconfigured: the picker shows "GIFs are not enabled on this server" and `onUnavailable` disables the composer's GIF button (with a `title` /`aria-label` reason) instead of failing silently. Returned media URLs are still pinned to the `klipy.com` CDN. |
2026-07-19 12:18:20 +00:00
### Quality tooling
2026-08-07 21:20:48 +02:00
224 test files (~83k LOC — about 2× the source): Vitest unit + integration
(70% coverage gate, blocking in CI), Playwright E2E (web suite in CI —
non-blocking full run plus a blocking `@parity` subset — and a native Tauri
suite that is deliberately not wired to CI), Stryker mutation testing
(manual-only), oxlint + type-checked ESLint, Prettier, Knip (non-blocking),
strict `tsc` . Rust: 84 `cargo test --lib` tests across 10 of the 16 modules,
blocking in CI together with `cargo clippy -D warnings` .
2026-07-19 12:18:20 +00:00
**Source of truth:** `src/main.ts` , `src/lib/dispatcher.ts` , `src/lib/ws.ts` ,
`src/lib/api.ts` , `src/lib/store.ts` , `src/stores/*.store.ts` ,
`src-tauri/src/lib.rs` , `src-tauri/tauri.conf.json` .