Merge pull request #1184 from J3vb/claude/blueprints-architectural-audit-k927qb

docs: architecture blueprints + 2026-07-19 spec-conformance audit
This commit is contained in:
J3vb
2026-07-19 14:43:02 +02:00
committed by GitHub
9 changed files with 877 additions and 0 deletions
+2
View File
@@ -207,6 +207,8 @@ When rotating the server updater key, update [Server/updater/server_update_publi
- [docs/livekit-setup.md](docs/livekit-setup.md)
- [docs/port-forwarding.md](docs/port-forwarding.md)
- [docs/tailscale.md](docs/tailscale.md)
- [docs/architecture/](docs/architecture/README.md) — system blueprints (diagrams + flows)
- [docs/audit-2026-07-19.md](docs/audit-2026-07-19.md) — latest architecture & spec-conformance audit
- [docs/api.md](docs/api.md)
- [docs/protocol.md](docs/protocol.md)
- [docs/schema.md](docs/schema.md)
+40
View File
@@ -0,0 +1,40 @@
# OwnCord Architecture Blueprints
**Verified against:** commit `ddc49f0`, 2026-07-19
**Companion audit:** [docs/audit-2026-07-19.md](../audit-2026-07-19.md)
This directory is the curated architectural map of OwnCord — the "blueprints" for
the whole system. Every diagram is a Mermaid fenced block (GitHub renders these
natively) followed by a prose explanation and a **Source of truth** file list.
## Index
| Doc | Diagrams | Covers |
|-----|----------|--------|
| [system-overview.md](system-overview.md) | D1 System context, D8 Deployment topology | All processes, trust boundaries, ports, single-instance constraints |
| [server.md](server.md) | D2 Server package map, D3 REST request lifecycle | Go package structure, DB-access styles, middleware chain |
| [websocket.md](websocket.md) | D4 WS connect / replay / dispatch | Real-time engine: auth handshake, 3-tier reconnect replay, backpressure, V1/V2 dispatch |
| [data-model.md](data-model.md) | D5 Entity-relationship overview | All 23 tables from migrations 001015, grouped by domain |
| [voice-e2ee.md](voice-e2ee.md) | D6 Voice + E2EE flow | LiveKit token flow, loopback TLS tunnel, ECDH key-holder relay |
| [client.md](client.md) | D7 Client module map | Tauri client: bootstrap, dispatcher, stores, Rust sidecars |
## Maintenance rule
These documents are **curated, not generated**. The rule that keeps them honest:
> If a PR changes the *structure* of anything listed in a diagram's
> **Source of truth** list (new package, new table, new message type, changed
> flow), that PR updates the corresponding diagram in the same change.
Diagrams reference stable identifiers (package names, table names, message-type
strings) rather than line numbers wherever possible. Line-number evidence lives
in the dated audit reports, which are point-in-time snapshots by design.
## Relationship to other docs
- `docs/api.md`, `docs/protocol.md`, `docs/schema.md` are the *reference specs*
(request/response shapes, wire formats, DDL). These blueprints describe
*structure and flow*, not payload shapes. Known drift between the specs and
the code is catalogued in [audit-2026-07-19.md §2](../audit-2026-07-19.md).
- `docs/client-architecture.md` predates the abandonment of the Solid.js
migration; [client.md](client.md) reflects the current state.
+112
View File
@@ -0,0 +1,112 @@
# Client Architecture (Tauri)
**Verified against:** commit `ddc49f0`, 2026-07-19
Desktop client built on Tauri v2: a TypeScript webview (~31.7k LOC, vanilla TS —
no UI framework) plus ~2.3k LOC of Rust commands. 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.
> `docs/client-architecture.md` still describes a SolidJS-based client. The
> Solid migration was **abandoned** (per CHANGELOG); only a 154-LOC beachhead
> remains under `src/components/solid/`. This document reflects the actual
> state.
## 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)"]
WSP["ws_proxy.rs<br/>WSS + TOFU cert pinning"]
LKP["livekit_proxy.rs<br/>loopback TLS tunnel"]
CRED["credentials.rs<br/>OS keychain"]
PTT["ptt.rs<br/>push-to-talk polling"]
UPDC["update_commands.rs<br/>self-hosted updater"]
SET["commands.rs<br/>settings store (key allowlist)"]
end
subgraph comm ["Communication layer (src/lib)"]
API["api.ts<br/>REST client (tauri-plugin-http,<br/>allowSelfSigned=true)"]
WSC["ws.ts<br/>reconnect w/ backoff, seq replay,<br/>generation counters"]
DISP["dispatcher.ts<br/>~30 msg types → store mutators"]
LKS["livekitSession.ts (1.7k LOC)<br/>voice state machine + E2EE"]
end
subgraph state ["Stores (8 singletons)"]
AUTH2["auth"]
CHAN["channels"]
MSG["messages"]
MEM["members"]
VOICE["voice"]
DM["dm"]
ROLES["roles"]
UIS["ui"]
end
subgraph ui ["UI (imperative DOM)"]
CP["ConnectPage<br/>profiles + health polling"]
MP["MainPage<br/>SidebarArea / ChatArea /<br/>controllers"]
COMP["~30 component families<br/>message-list, settings tabs,<br/>voice widgets, overlays"]
SOLID["components/solid/<br/>abandoned beachhead"]
end
MAIN --> CP
MAIN --> MP
MAIN --> API
MAIN --> WSC
WSC --> WSP
WSC --> DISP
DISP --> AUTH2 & CHAN & MSG & MEM & VOICE & DM & ROLES & UIS
state --> ui
LKS --> LKP
VOICE --> LKS
MP --> COMP
API -.->|"no cert pinning<br/>(unlike WS path)"| SRV["Go server"]
WSP --> SRV
CRED -.-> MAIN
UPDC -.-> MAIN
%% cross-store coupling (audit finding)
AUTH2 -.->|clearAuth → leaveVoice| VOICE
VOICE -.-> MEM
classDef dead fill:none,stroke-dasharray: 5 5,opacity:0.6
class SOLID dead
```
**What this shows.** Data flows one way in the happy path: WS frame → Rust
`ws_proxy``ws.ts``dispatcher.ts` → store mutators → subscribed components
re-render. The dashed edges mark the audit findings: the HTTP path accepts any
certificate (`allowSelfSigned` hardcoded, no TOFU pinning — unlike the WS and
LiveKit paths, which pin fingerprints in Rust), the stores cross-import each
other (auth→voice→members), and the Solid beachhead is dead weight.
### 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 |
| Cert trust | `src-tauri/src/ws_proxy.rs` | TOFU: first fingerprint pinned per host (`certs.json`); mismatch → modal (`CertMismatchModal`) |
| 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 |
| 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 |
### Quality tooling
157 test files (~63k LOC — about 2× the source): Vitest unit + integration,
Playwright E2E (web and native Tauri suites), Stryker mutation testing, oxlint +
type-checked ESLint, Prettier, Knip, strict `tsc`. The client unit suite is
currently marked "KNOWN RED" and non-blocking in CI — an audit finding.
**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`.
+126
View File
@@ -0,0 +1,126 @@
# Data Model
**Verified against:** commit `ddc49f0`, 2026-07-19
The canonical schema is the ordered migration set `Server/migrations/001015`
(embedded via `go:embed`, applied by the custom runner in `Server/db/migrate.go`,
tracked in the `schema_versions` table). SQLite is the only supported engine —
`Server/main.go` rejects any other `database.type` at startup.
## D5 — Entity-relationship overview
All 23 application tables, grouped by domain. Junction/leaf detail columns are
elided; the goal is the relationship graph, not full DDL (see `docs/schema.md`
for DDL — note it is currently 6 migrations behind, see
[audit-2026-07-19.md §2](../audit-2026-07-19.md)).
```mermaid
erDiagram
%% ── Identity & access ──
roles ||--o{ users : "role_id"
users ||--o{ sessions : "user_id"
roles ||--o{ channel_overrides : "role_id"
channels ||--o{ channel_overrides : "channel_id"
users ||--o{ user_blocks : "blocker_id / blocked_id"
users ||--o{ invites : "created_by / redeemed_by"
%% ── Messaging ──
channels ||--o{ messages : "channel_id"
users ||--o{ messages : "user_id"
messages ||--o{ messages : "reply_to"
messages ||--o{ attachments : "message_id"
messages ||--o{ reactions : "message_id"
users ||--o{ reactions : "user_id"
users ||--o{ read_states : "user_id"
channels ||--o{ read_states : "channel_id"
%% ── Direct messages (channels with type='dm') ──
channels ||--o{ dm_participants : "channel_id"
users ||--o{ dm_participants : "user_id"
channels ||--o{ dm_open_state : "channel_id"
users ||--o{ dm_open_state : "user_id"
%% ── Voice ──
users ||--o| voice_states : "user_id (PK)"
channels ||--o{ voice_states : "channel_id"
%% ── Plugins ──
plugins ||--o{ plugin_kv : "plugin_id"
%% ── Standalone (no FK edges) ──
events
settings
audit_log
audit_log_v6
login_attempts
rate_lockouts
emoji
sounds
users {
int id PK
int role_id FK
string username
string password_hash
bool banned
}
roles {
int id PK
string name
int permissions "31-bit bitfield"
int position
}
channels {
int id PK
string name
string type "text | voice | dm (trigger-enforced)"
}
messages {
int id PK
int channel_id FK
int user_id FK
int reply_to FK
string content
}
attachments {
string id PK "UUID"
int message_id FK
int uploader_id "added by 010"
}
events {
int seq PK "AUTOINCREMENT, hub seq seeded from MAX(seq)"
string type
string payload
}
```
### Domain notes
| Domain | Tables | Notes |
|--------|--------|-------|
| Identity & access | `roles`, `users`, `sessions`, `channel_overrides`, `user_blocks`, `invites`, `login_attempts`, `rate_lockouts` | Sessions store only SHA-256 token hashes. Permissions are a bitfield on `roles.permissions`; channel overrides use Discord semantics `(role &^ deny) \| allow`. `rate_lockouts` (011) persists rate-limiter lockouts across restarts. |
| Messaging | `channels`, `messages`, `attachments`, `reactions`, `read_states`, `emoji` | `channels.type` is constrained to `text \| voice \| dm` by INSERT/UPDATE triggers from migration 013 — the `announcement` type in the specs/admin UI is rejected at this layer. `attachments.uploader_id` (010) backs upload-ownership checks. |
| Direct messages | `dm_participants`, `dm_open_state` | DMs are `channels` rows with `type='dm'`; these tables track membership and per-user open/closed UI state (009). |
| Voice | `voice_states` | One row per user (`user_id` is the PK) — a user occupies at most one voice channel. |
| Real-time replay | `events` | Cold tier of the 3-tier reconnect replay ([websocket.md](websocket.md)); written by the async `EventPersister`, pruned by retention. Hub seq counter is seeded from `MAX(events.seq)` at startup so seqs stay monotonic across restarts. |
| Plugins | `plugins`, `plugin_kv` | 015. `plugin_kv` is per-plugin namespaced KV via composite PK `(plugin_id, key)`. |
| Ops | `settings`, `audit_log`, `audit_log_v6`, `sounds` | `settings` is a generic KV read by admin and (directly, via inline SQL) by the WS hub. `audit_log` + `audit_log_v6` coexist after the 003 rebuild. `sounds` is **dead schema** — the soundboard feature was removed but the table remains. |
### How the schema is accessed (three coexisting styles)
1. **Raw SQL in `Server/db`** — hand-written queries (`*_queries.go`, ~178 call
sites). This is what actually runs, used directly by `Server/api` handlers,
`Server/admin`, and the `ws.Hub`.
2. **`store.Store` interface** (`Server/store`) — a 14-domain composed interface
with `SQLiteStore` (delegates to `db.DB`) and `MemStore` (test double).
Consumed by the `Server/service` layer only.
3. **sqlc-generated `Server/db/dbgen`** (~3.5k LOC, from `Server/db/queries/sqlite/`
per `sqlc.yaml`) — **imported by nothing**; dead code kept verified by the
`sqlc-verify` CI job.
The coexistence of all three is the largest structural finding of the audit —
see [audit-2026-07-19.md §3](../audit-2026-07-19.md).
**Source of truth:** `Server/migrations/*.sql` (schema), `Server/db/migrate.go`
(runner), `Server/db/*_queries.go` (live queries), `Server/store/store.go`
(interface), `sqlc.yaml` + `Server/db/dbgen/` (dormant generated layer).
+134
View File
@@ -0,0 +1,134 @@
# Server Architecture
**Verified against:** commit `ddc49f0`, 2026-07-19
Single Go binary (`github.com/owncord/server`, Go 1.25). Pure-Go SQLite
(`modernc.org/sqlite`, no CGO), chi router, `nhooyr.io/websocket`, LiveKit for
voice, Wazero for plugins (build-tag gated), optional OpenTelemetry
(`-tags otel`). Roughly 29k LOC of production code and 46k LOC of tests.
## D2 — Package map
```mermaid
flowchart TB
subgraph entry ["Process entry"]
MAIN["main.go<br/>config, TLS, DB open+migrate,<br/>signal handling, maintenance loop"]
end
subgraph http ["HTTP layer"]
API["api<br/>router, middleware, REST handlers,<br/>WAF, LiveKit proxy, uploads"]
ADMIN["admin<br/>embedded SPA + admin REST,<br/>SSE log stream"]
end
subgraph realtime ["Real-time"]
WS["ws<br/>Hub, pub/sub, replay,<br/>V1+V2 dispatch, LiveKit, E2EE relay"]
end
subgraph domain ["Domain"]
SVC["service<br/>Message/Channel/Permission/User/<br/>DM/Invite/Block/Moderation/Voice"]
PERM["permissions<br/>bitfield + Checker"]
PLUGIN["plugin<br/>wazero runtime, registry,<br/>manifest, host APIs"]
end
subgraph data ["Data"]
STORE["store<br/>Store interface,<br/>SQLiteStore, MemStore"]
DB[("db<br/>raw SQL queries,<br/>migration runner, models")]
DBGEN["db/dbgen<br/>sqlc-generated"]
MIG["migrations<br/>001015 embedded SQL"]
end
subgraph support ["Support"]
AUTH["auth<br/>bcrypt, tokens, TOTP,<br/>rate limiting, TLS"]
CFG["config<br/>koanf: defaults→YAML→env"]
STORAGE["storage<br/>upload files on disk"]
TEL["telemetry<br/>OTel (no-op default)"]
UPD["updater<br/>minisign-verified self-update"]
end
MAIN --> CFG
MAIN --> API
MAIN --> DB
MAIN --> STORE
API --> ADMIN
API --> WS
API --> SVC
API --> AUTH
API --> STORAGE
API --> UPD
API --> PLUGIN
SVC --> STORE
SVC --> PERM
STORE --> DB
DB --> MIG
WS --> SVC
WS --> PLUGIN
%% Layering violations (dashed red): raw *db.DB used above the store seam
API -.->|"handlers take *db.DB directly"| DB
ADMIN -.->|"raw *db.DB"| DB
WS -.->|"inline SQL for settings"| DB
DBGEN -.-|"generated but imported by nothing"| DB
classDef dead fill:none,stroke-dasharray: 5 5,opacity:0.6
class DBGEN dead
```
**What this shows.** The intended layering is
`api → service → store → db`, and the `service` layer does follow it. But three
dashed edges mark where the seam is bypassed: nearly every REST handler receives
both `svc` *and* a raw `*db.DB`; the `admin` package operates on `*db.DB`
almost exclusively; and the `ws.Hub` runs inline SQL against the `settings`
table. `db/dbgen` (sqlc output) is generated and CI-verified but referenced by
no code. In effect the server has three coexisting data-access styles — see
[data-model.md](data-model.md) and the audit for the consolidation
recommendation.
`api.NewRouter` (`Server/api/router.go`) is the composition root: it constructs
the rate limiter, TOTP key, storage, `service.New`, the `ws.Hub`, the LiveKit
client/subprocess, the updater, the admin handler, and the plugin admin handler;
spawns background goroutines; and mounts all routes. `main.go` performs only
process-level wiring (config, TLS, DB, event persistence, HTTP server,
shutdown).
**Source of truth:** `Server/main.go`, `Server/api/router.go`, package import
graph (`go list -deps`), `Server/store/store.go`, `sqlc.yaml`.
## D3 — REST request lifecycle
```mermaid
sequenceDiagram
autonumber
participant C as Client
participant MW as Global middleware<br/>(api/router.go)
participant RT as Route mount<br/>(Mount*Routes)
participant H as Handler
participant S as service.*
participant ST as store.Store
participant DB as SQLite
C->>MW: HTTPS request
Note over MW: RequestID → Recoverer → requestLogger<br/>→ telemetry → SecurityHeadersWithTLS<br/>→ MaxBodySizeUnless(uploads exempt)<br/>→ optional Coraza WAF
MW->>RT: routed by chi
Note over RT: AuthMiddleware(database)<br/>+ per-route rate limits<br/>+ RequirePermission(...) where mounted
RT->>H: authenticated request
H->>S: domain call (svc.Messages, svc.Permissions, …)
S->>ST: Store interface method
ST->>DB: SQL (via db.DB)
DB-->>C: JSON response (errorResponse envelope on failure)
rect rgba(200,120,120,0.15)
Note over H,DB: Deviation — auth routes: MountAuthRoutes(r, database, …)<br/>bypasses service/store and queries *db.DB directly.<br/>Admin REST (Server/admin) does the same behind<br/>AdminIPRestrict + RequireAdminAuth.
end
```
**What this shows.** The global chain is assembled in `NewRouter`; note that
chi's `middleware.RealIP` is deliberately omitted — client IP is resolved via
`clientIPWithProxies` against configured trusted proxies instead, so spoofed
`X-Real-IP`/`X-Forwarded-For` headers are not trusted by default. Authentication
is bearer-token (SHA-256-hashed opaque tokens); authorization is enforced
inconsistently — sometimes as `RequirePermission` middleware at mount time,
sometimes in-handler through `svc.Permissions` (an audit finding). The shaded
region marks the two documented bypass paths of the domain layer.
**Source of truth:** `Server/api/router.go`, `Server/api/middleware.go`,
`Server/api/auth_handler.go`, `Server/admin/middleware.go`, `Server/service/`.
+99
View File
@@ -0,0 +1,99 @@
# System Overview
**Verified against:** commit `ddc49f0`, 2026-07-19
OwnCord is a self-hosted chat stack: one Go server binary per community, a Tauri
desktop client that can hold profiles for many servers (one active connection at
a time), LiveKit for voice/video media, and an embedded web admin panel.
## D1 — System context and trust boundaries
```mermaid
flowchart LR
subgraph desktop ["Desktop client (Tauri v2)"]
WV["Webview (TS)<br/>UI, stores, dispatcher"]
subgraph sidecars ["Rust commands"]
WSP["ws_proxy<br/>TOFU-pinned WSS"]
LKP["livekit_proxy<br/>TOFU-pinned TLS tunnel"]
CRD["credentials<br/>OS keychain"]
UPD["updater<br/>pinned TLS + minisign"]
end
WV --- sidecars
end
subgraph server ["Self-hosted Go server (single binary)"]
RTR["api router<br/>REST /api/v1"]
HUB["ws Hub<br/>real-time"]
ADM["admin SPA + REST<br/>(IP-gated)"]
PLG["plugin runtime<br/>(wazero, opt-in)"]
DBF[("SQLite file<br/>WAL, single writer")]
UPS["file storage<br/>uploads/"]
end
LK["LiveKit server<br/>(managed subprocess<br/>or external)"]
REL["OwnCord-releases<br/>(GitHub, minisign-signed)"]
WV -->|"HTTPS REST<br/>⚠ accepts any cert<br/>(no pinning)"| RTR
WSP -->|"WSS, fingerprint-pinned"| HUB
LKP -->|"TLS, fingerprint-pinned"| LK
WV -->|"admin panel (browser)"| ADM
HUB <-->|"webhooks + server SDK"| LK
RTR --> DBF
HUB --> DBF
RTR --> UPS
PLG -.->|"allowlisted HTTP only"| NET["external hosts"]
UPD -->|"via connected server URL"| REL
server -->|"self-update check"| REL
```
**What this shows.** Three transport paths leave the client and only two are
certificate-pinned: the app WebSocket and the LiveKit tunnel go through Rust
proxies that pin a trust-on-first-use SHA-256 fingerprint per host; the HTTP
REST path currently accepts any certificate (an acknowledged gap tracked in the
audit). The admin panel is served by the same binary but gated to configured
CIDRs (private ranges by default), with bearer admin auth on top for the plugin
endpoints. Plugins run in a WASM sandbox whose HTTP capability is allowlisted
per manifest. Both the server self-updater and the client updater verify
minisign signatures against pinned embedded public keys.
## D8 — Deployment topology
```mermaid
flowchart TB
subgraph hostbox ["Operator host (or Docker)"]
BIN["owncord server binary"]
BIN --> CFGF["config.yaml<br/>(koanf: defaults → YAML → OWNCORD_* env)"]
BIN --> DATA["data dir<br/>SQLite DB + uploads + TLS certs"]
BIN --> LKPROC["livekit-server<br/>(optional managed subprocess)"]
BIN --> P1[":8443 HTTPS + WSS<br/>API, WS, admin, uploads"]
BIN -.-> P80[":80 ACME HTTP-01<br/>(when TLS mode acme)"]
LKPROC --> P2["LiveKit ports<br/>(WS + UDP media range)"]
end
C1["Tauri clients"] --> P1
C1 --> P2
ADMB["Admin browser<br/>(allowed CIDRs only)"] --> P1
subgraph constraints ["Single-instance constraints (scale-out blockers)"]
R1["in-memory rate-limiter windows<br/>(lockouts persisted, windows not)"]
R2["in-memory pub/sub + replay ring buffer"]
R3["process-local TOTP replay store"]
R4["SQLite single-writer (MaxOpenConns=1)"]
end
BIN --- constraints
```
**What this shows.** The deployment unit is one process per community —
TLS (self-signed, custom, or ACME), the DB, uploads, the admin panel, and
optionally LiveKit are all owned by that process. The design is explicitly
single-instance: rate-limit windows, pub/sub, the replay ring buffer, and the
TOTP replay store are process-local, and SQLite runs with a single writer.
Horizontal scaling is out of scope today; the constraint boxes name exactly
what would have to move to shared infrastructure if that ever changes. A
15-minute maintenance goroutine (expired sessions, orphaned attachments, with a
circuit breaker) and graceful drain on SIGINT/SIGTERM round out the process
lifecycle.
**Source of truth:** `Server/main.go`, `Server/config/config.go`,
`Server/docker-compose.yml`, `docs/deployment.md`, `docs/server-configuration.md`,
`Client/tauri-client/src-tauri/src/lib.rs`.
+73
View File
@@ -0,0 +1,73 @@
# Voice and End-to-End Encryption
**Verified against:** commit `ddc49f0`, 2026-07-19
Voice/video runs on LiveKit. The Go server issues short-lived scoped tokens and
relays E2EE key-exchange messages; media flows client↔LiveKit directly. On the
client, everything funnels through `src/lib/livekitSession.ts`, and — because
self-hosted servers commonly use self-signed certificates — the LiveKit
connection is tunneled through a local Rust TLS proxy pinned to the same TOFU
fingerprint as the main WebSocket.
## D6 — Voice join + E2EE key exchange
```mermaid
sequenceDiagram
autonumber
participant UI as Client UI
participant LKS as livekitSession.ts
participant RP as Rust livekit_proxy<br/>(loopback TCP→TLS, TOFU-pinned)
participant WS as App WebSocket (Hub)
participant SRV as Go server
participant LK as LiveKit server
UI->>WS: voice_join {channel_id}
WS->>SRV: permission check (channel-scoped)
SRV-->>WS: voice_token (5-min JWT,<br/>CanPublishSources scoped by permission)
WS-->>LKS: voice_token payload
LKS->>RP: connect ws://127.0.0.1:{port}
RP->>LK: TLS (fingerprint-pinned)
LKS->>LK: LiveKit signaling + media (via tunnel)
rect rgba(120,160,220,0.15)
Note over LKS,WS: E2EE key exchange (relayed via app WS)
LKS->>WS: voice_e2ee_announce {ECDH pubkey}
WS-->>LKS: voice_e2ee_announce broadcast to channel
Note over SRV: Hub tracks per-channel key holder<br/>(lowest user ID)
LKS->>WS: voice_e2ee_offer {wrapped room key, target user}
WS-->>LKS: voice_e2ee_offer relayed to target
Note over LKS: unwrap room key → LiveKit<br/>ExternalE2EEKeyProvider
Note over LKS: on participant leave,<br/>key holder rotates room key
end
```
**What this shows.** The server never holds the room key — it only relays
announce/offer messages and tracks who the key holder is (deterministically the
lowest user ID in the channel). Keys are wrapped per-recipient via ECDH, and
the key holder rotates the room key when a participant leaves so departed
members cannot decrypt future media. Voice permission enforcement happens twice:
at `voice_join` (channel permission) and inside the LiveKit JWT itself
(`CanPublishSources` restricts camera/screenshare per role permission).
Supporting pieces:
- **Server:** `Server/ws/voice_e2ee.go` (relay + key-holder map),
`Server/ws/livekit.go` (token minting), `Server/ws/livekit_process.go`
(optional managed `livekit-server` subprocess),
`Server/ws/livekit_webhook.go` (webhook validated by LiveKit JWT and
admin-IP-restricted), `Server/api/livekit_proxy.go` (HTTP reverse proxy).
- **Client:** `src/lib/livekitSession.ts` (state machine: idle/connecting/
connected/reconnecting with a monotonic `joinGeneration` to discard
superseded joins), `src/lib/e2eeCrypto.ts` (ECDH, key wrap/unwrap),
`src/lib/audioPipeline.ts` + `src/lib/noise-suppression.ts` (RNNoise WASM),
`src/lib/screenShare.ts`, `src-tauri/src/livekit_proxy.rs` (tunnel),
`src-tauri/src/ptt.rs` (push-to-talk key polling).
This message flow (`voice_e2ee_announce` / `voice_e2ee_offer` /
`voice_speakers`) is currently **absent from `docs/protocol.md`** — recorded as
spec drift in [audit-2026-07-19.md §2](../audit-2026-07-19.md).
**Source of truth:** `Server/ws/voice_e2ee.go`, `Server/ws/livekit.go`,
`Client/tauri-client/src/lib/livekitSession.ts`,
`Client/tauri-client/src/lib/e2eeCrypto.ts`,
`Client/tauri-client/src-tauri/src/livekit_proxy.rs`.
+114
View File
@@ -0,0 +1,114 @@
# WebSocket / Real-time Engine
**Verified against:** commit `ddc49f0`, 2026-07-19
The `Server/ws` package (~6.9k LOC, the largest in the server) implements the
real-time engine: a single `Hub` owning all client connections, a topic-based
pub/sub, a monotonic sequence counter, a 3-tier reconnect replay pipeline, and a
dual (V1 legacy + V2 typed) command dispatch. Message-type constants live in
`Server/ws/message_types.go` (client↔server) and mirror
`Client/tauri-client/src/lib/protocolTypes.ts`.
> Both files claim to be "Generated from docs/protocol-schema.json — single
> source of truth", but **no such file exists in the repository** — the
> constants are maintained by hand on both sides. See
> [audit-2026-07-19.md §3](../audit-2026-07-19.md).
## D4a — Connect, authenticate, replay
```mermaid
sequenceDiagram
autonumber
participant C as Client (ws.ts via Rust ws_proxy)
participant S as ws.ServeWS
participant H as Hub
C->>S: WSS upgrade /api/v1/ws (Origin checked)
Note over S: no HTTP AuthMiddleware —<br/>auth is in-band, 10s deadline
C->>S: {type:"auth", payload:{token, last_seq}}
S->>S: validate token hash → session expiry → user → ban
S->>H: register (kicks previous conn of same user)
S-->>C: auth_ok {user, server_name, motd, replay_source}
alt last_seq within in-memory ring buffer (Tier 1)
H-->>C: replay EventsSinceFiltered (perm-filtered, fail-closed)
else last_seq within events table (Tier 2, max 5000)
H-->>C: replay from cold-tier EventStore
else too far behind, or channel visibility changed (Tier 3)
H-->>C: full "ready" re-sync snapshot
end
loop steady state
C->>H: chat_send / reaction_add / voice_join / …
H-->>C: seq-stamped broadcasts (chat_message, presence, …)
C->>S: ping (every 30s) → pong
end
```
**What this shows.** Auth is deliberately in-band (the WS route mounts without
`AuthMiddleware`). Every broadcast is assigned a monotonic `seq` under a
dedicated mutex; the client reports its `last_seq` on reconnect and the hub
picks the cheapest replay tier. A `visibilityChangeSeq` watermark forces a full
re-sync whenever channel visibility changed while the client was away, so
permission changes can never be replayed around. `auth_ok.replay_source`
(`none|buffer|db`) reports which tier served the reconnect and feeds the
`ws_reconnect_tier_total` metric.
## D4b — Broadcast fanout and backpressure
```mermaid
flowchart LR
EV["deliverBroadcast<br/>assign seq"] --> RB["EventRingBuffer<br/>(1000, Tier 1)"]
EV --> EP["EventPersister<br/>async batched → events table<br/>(Tier 2; drops if queue full)"]
EV --> PLG["plugin EventSink"]
EV --> PS["PubSub topics<br/>global / channel:N / voice:N / user:N<br/>(per-topic 100 msg/s limit)"]
PS --> CH{"per-client queues"}
CH --> HI["sendHigh (64)<br/>DMs, mentions"]
CH --> NO["send (256)<br/>chat, reactions"]
CH --> LO["sendLow (64)<br/>typing, presence"]
HI --> WP["writePump<br/>drains high-first"]
NO --> WP
LO --> WP
WP -->|"high/normal full →<br/>disconnect (forces replay)"| X["client"]
LO -.->|"low full → silently dropped"| X
```
**What this shows.** Overflow policy is intentional: dropping a chat message
would corrupt state, so a full normal/high queue disconnects the client and the
replay pipeline restores consistency; typing/presence are lossy by design. The
global `broadcast` channel (1024) drops with a `broadcastDrops` counter when
saturated.
## D4c — Dual dispatch (V1 → V2 strangler-fig)
```mermaid
stateDiagram-v2
[*] --> handleMessage
handleMessage --> V2: registry.hasV2(type)
handleMessage --> V1: no V2 handler
V2: V2 typed command path
V2: strict parse → Command → Result{mutations, events}
V1: V1 legacy path
V1: lenient parse → registry.Dispatch → imperative handler
V2 --> [*]
V1 --> [*]
```
**What this shows.** Both dispatch generations are registered in `NewHub` and
live simultaneously; each message type is tried against the stricter V2 typed
path first and falls back to V1. Two parsers and two handler registries must be
kept in sync until the migration completes — tracked as an audit finding.
The `Hub` also owns: stale-client sweep (90s), revoked-session sweep (30s, plus
per-connection revalidation every 10 messages), stale-voice-state sweep (60s),
panic containment on the run loop (3 panics/60s → stop), LiveKit client and
optional managed subprocess, and the voice E2EE key-holder map
([voice-e2ee.md](voice-e2ee.md)). Many collaborators are attached
post-construction via `SetLiveKit` / `SetEventPersister` /
`SetPluginRegistry` setters that "must be called before Run" — temporal
coupling noted in the audit.
**Source of truth:** `Server/ws/hub.go`, `Server/ws/serve.go`,
`Server/ws/client.go`, `Server/ws/handlers.go`, `Server/ws/command.go`,
`Server/ws/pubsub.go`, `Server/ws/ringbuffer.go`, `Server/ws/event_persister.go`,
`Server/ws/message_types.go`, `Server/migrations/014_events_table.sql`.
+177
View File
@@ -0,0 +1,177 @@
# OwnCord — Architectural Audit & Spec-Conformance Review
**Date:** 2026-07-19
**Branch:** claude/blueprints-architectural-audit-k927qb (audited tree: `ddc49f0` = main)
**Scope:** whole-system architecture mapping + code-by-spec conformance. Read-only — no code or spec changes ship with this audit; the companion blueprint set lives in [docs/architecture/](architecture/README.md).
**Relationship to prior audit:** successor to [audit-2026-04-07.md](audit-2026-04-07.md), which remains the closure tracker for its own findings. Carried-over items are re-verified in §1, not restated.
---
## Finding closure status (maintained; update statuses in place)
Standing rule: every HIGH below gets either a closing commit link or an explicit
accepted-risk note before the beta gate. MEDIUMs are folded into the backlog
(§6) and closed opportunistically.
| ID | Sev | Finding | Status |
|----|-----|---------|--------|
| A-2026-07-01 | HIGH | `announcement` channel type: documented in 3 specs and offered by the admin API, but hard-rejected by DB triggers | OPEN — needs a product decision (implement or remove) |
| A-2026-07-02 | HIGH | Client HTTP path accepts any TLS certificate (`allowSelfSigned` hardcoded; no TOFU pinning, unlike WS/LiveKit paths) | OPEN — acknowledged in code comments |
| A-2026-07-03 | HIGH | Reference specs (api.md / protocol.md / schema.md) frozen at 2026-04-02; systemic drift incl. whole undocumented subsystems (voice E2EE, plugins) | OPEN — refresh proposed as one PR (§6 item 4) |
| A-2026-07-04 | HIGH | Client unit test suite "KNOWN RED" and non-blocking in CI; E2E never gated | OPEN — supersedes prior #11's scope |
| A-2026-07-05 | MEDIUM | Dead sqlc layer: `Server/db/dbgen/` (~3.5k LOC) generated + CI-verified but imported by nothing | OPEN |
| A-2026-07-06 | MEDIUM | Three coexisting DB-access styles (raw `*db.DB` in api/admin/ws, `store.Store` under service, dead dbgen) | OPEN |
| A-2026-07-07 | MEDIUM | Channel-visibility logic duplicated across ~4 sites with "must mirror" comments | OPEN |
| A-2026-07-08 | MEDIUM | Protocol constants on both sides claim generation from `docs/protocol-schema.json`, which does not exist in the repo | OPEN |
| A-2026-07-09 | MEDIUM | Dual V1+V2 WS dispatch (strangler-fig) still live; two parsers/registries to keep in sync | OPEN |
| A-2026-07-10 | MEDIUM | `api.NewRouter` god-constructor: builds services, hub, LiveKit, updater, admin, plugins; spawns goroutines; mounts everything | OPEN |
| A-2026-07-11 | MEDIUM | `ws.Hub` mega-object with post-construction `Set*` wiring ("must be called before Run") | OPEN |
| A-2026-07-12 | MEDIUM | Abandoned SolidJS beachhead still in-tree; `docs/client-architecture.md` describes the abandoned architecture | OPEN |
| A-2026-07-13 | LOW | Dead schema: `sounds` table survives soundboard removal; `audit_log` + `audit_log_v6` coexist | OPEN |
| A-2026-07-14 | LOW | Scattered client constants (`#5865F2` ×18, `localhost:8443` ×3); 64 timer call sites with manual lifecycle | OPEN |
| A-2026-07-15 | LOW | `docs/plans/security-hardening-remediation.md` partly stale (references deleted `store/postgres.go`) | OPEN |
---
## Table of Contents
1. [Carried-over items from audit-2026-04-07](#1-carried-over-items-from-audit-2026-04-07)
2. [Spec-conformance matrix](#2-spec-conformance-matrix)
3. [Server architecture findings](#3-server-architecture-findings)
4. [Client architecture findings](#4-client-architecture-findings)
5. [Process & CI findings](#5-process--ci-findings)
6. [Prioritized improvement backlog](#6-prioritized-improvement-backlog)
---
## 1. Carried-over items from audit-2026-04-07
Re-verified in today's tree. Statuses below reflect the code, not the prior
audit's table; details stay in [audit-2026-04-07.md](audit-2026-04-07.md).
| Prior # | Sev | Finding (one-line) | Re-verification (2026-07-19) |
|---------|-----|--------------------|------------------------------|
| 15 | CRITICAL | Plugin governance (timeout, storage isolation, ACL, event rate limit, HTTP exfiltration) | Unchanged since prior closure table; plugins still default-disabled (`plugins.enabled: false`), which is the standing mitigation |
| 6 | HIGH | `Server/store/` untested | **Still zero `_test.go` files** in `Server/store/`. The "SUPERSEDED — remove in P4" plan has not executed; the package remains the designated abstraction seam with no direct tests |
| 7 | HIGH | Client unit coverage | Suite is large (157 test files) but currently KNOWN RED and non-blocking — see A-2026-07-04 |
| 9 | MEDIUM | auth_handler bypasses service layer | **Confirmed open**`Server/api/router.go:101` passes `database *db.DB` to `MountAuthRoutes` while sibling mounts receive `svc` |
| 10 | MEDIUM | Audit-trail write failures silently ignored | **Confirmed open**`Server/admin/handlers_backup.go:55` and `:139` still discard the error: `_ = database.LogAudit(...)` |
| 11 | MEDIUM | E2E not in CI | **Confirmed open** — no Playwright job exists in `.github/workflows/ci.yml` |
| W3-4 (remediation plan) | LOW | Contradictory upload cache header | **Confirmed open**`Server/api/upload_handler.go:309` sets `private, max-age=%d, no-cache` (max-age and no-cache contradict) |
---
## 2. Spec-conformance matrix
The three reference specs were last meaningfully edited **2026-04-02**
(`git log` on each file); the code is at v1.1.0-alpha.2 with substantial July
2026 changes. Resolution column: **fix-spec** (doc catches up to code),
**fix-code** (code is wrong), **decide** (product decision needed first).
### 2.1 `docs/api.md`
| ID | Spec says | Code does | Evidence | Sev | Resolution |
|----|-----------|-----------|----------|-----|------------|
| B | `GET /api/v1/info` returns `{name, version}` | `version` removed (anti-fingerprinting, "C-2") | `Server/api/router.go` `infoResponse{Name}` only | MEDIUM | fix-spec |
| C | `GET /health` returns `version` | `version` removed for the same reason | `Server/api/router.go` `healthResponse{Status, Uptime, OnlineUsers}` | MEDIUM | fix-spec |
| F | — (absent) | Profile surface exists: `PATCH /api/v1/users/me`, `PUT /users/me/password`, `GET`/`DELETE /users/me/sessions` | `Server/api/profile_handler.go` (`MountProfileRoutes`) | MEDIUM | fix-spec |
| G | — (absent) | Plugin admin REST surface: `/api/v1/admin/plugins` (list/enable/disable/uninstall/install-zip) | `Server/api/router.go:250` | MEDIUM | fix-spec |
### 2.2 `docs/protocol.md`
| ID | Spec says | Code does | Evidence | Sev | Resolution |
|----|-----------|-----------|----------|-----|------------|
| D1 | — (voice E2EE absent) | Full E2EE signaling: client `voice_e2ee_announce`/`voice_e2ee_offer`, server broadcast/relay of both | `Server/ws/message_types.go`, `Server/ws/voice_e2ee.go`; flow in [architecture/voice-e2ee.md](architecture/voice-e2ee.md) | HIGH | fix-spec |
| D2 | — (absent) | `voice_speakers` server event | `Server/ws/message_types.go` (`MsgTypeVoiceSpeakers`) | LOW | fix-spec |
| D3 | — (absent) | `user_update` broadcast on profile change | `Server/ws/message_types.go`; wired via `MountProfileRoutes` broadcaster | LOW | fix-spec |
| D4 | `member_leave` appears only in the seq table | Fully implemented broadcast | `Server/ws/message_types.go` (`MsgTypeMemberLeave`) | LOW | fix-spec |
| E | `auth_ok` payload = `{user, server_name, motd}` | Also carries `replay_source` (`none\|buffer\|db`) | `Server/ws/serve.go` auth path; CHANGELOG Phase B | LOW | fix-spec |
### 2.3 `docs/schema.md`
| ID | Spec says | Code does | Evidence | Sev | Resolution |
|----|-----------|-----------|----------|-----|------------|
| A | Channel types: `text, voice, announcement, dm` (also in api.md and protocol.md; admin API offers `announcement`) | DB triggers **reject** any type outside `text\|voice\|dm` | `Server/migrations/013_channel_type_constraint.sql` vs `Server/admin/handlers_channels.go` | **HIGH** | **decide** — either implement announcement channels end-to-end or remove the type from specs + admin API |
| H | Migration history stops at "008", with wrong numbering (two `003_*` entries; DM tables labeled 008) | 15 migrations exist, `001``015`; real order diverges from 004 onward | `Server/migrations/` directory listing | MEDIUM | fix-spec |
| I | — (absent) | 9 tables undocumented: `login_attempts`, `settings`, `emoji`, `sounds`, `rate_lockouts`, `user_blocks`, `events`, `plugins`, `plugin_kv` | `Server/migrations/001,011,012,014,015` — see [architecture/data-model.md](architecture/data-model.md) | MEDIUM | fix-spec |
| J | attachments DDL without `uploader_id` | Column added for upload-ownership checks | `Server/migrations/010_attachment_uploader.sql` | MEDIUM | fix-spec |
| K | — (sqlc unmentioned) | `sqlc.yaml` + `Server/db/dbgen/` exist (currently dead — A-2026-07-05) | `sqlc.yaml`, `Server/db/dbgen/` | LOW | fix-spec (document whichever way A-2026-07-05 resolves) |
**Systemic conclusion:** drift is not isolated typos — entire subsystems
(E2EE signaling, plugins, six migrations) postdate the specs. Item A is the
only case where *code and spec actively contradict at runtime*: an admin can
select a channel type the database will refuse to insert. Recommended handling:
one coherent spec-refresh PR (backlog item 4) rather than piecemeal edits, plus
a decision on A first.
---
## 3. Server architecture findings
Verdict: the intended layering (`api → service → store → db`) is sound and the
`service` layer respects it; test discipline is excellent (test LOC ≈ 1.6×
source; race + deadlock + mutation tooling). The findings are about the seams
that grew around that design.
| ID | Sev | Area | Evidence | Finding | Recommendation | Effort |
|----|-----|------|----------|---------|----------------|--------|
| A-2026-07-05 | MEDIUM | Data layer | `Server/db/dbgen/` (~3.5k LOC), `sqlc.yaml`, CI `sqlc-verify` job | sqlc output is generated, version-pinned, CI-verified — and imported by nothing. Hand-written raw SQL in `Server/db/*_queries.go` is what runs. | Decide the Phase-A question: adopt dbgen inside `db.DB` method bodies, or delete `dbgen/` + `queries/` + the CI job. Either ends the illusion of a second data layer. | S |
| A-2026-07-06 | MEDIUM | Layering | All `Mount*Routes` signatures take `database *db.DB` alongside `svc` (`Server/api/*_handler.go`); `Server/admin` handlers take `*db.DB` | ~359 direct `database.*` calls above the store seam; three access styles coexist. The abstraction exists but cannot be relied on (e.g. for a future backend swap or for test doubles). | Consolidate incrementally: new handlers service-only; migrate one mount per PR, starting with auth (prior #9). | L |
| A-2026-07-07 | MEDIUM | Correctness risk | `Server/ws/serve.go` (`buildReady`, `computeAllowedChannels`), `Server/ws/hub.go` (`RefreshChannelVisibility`), REST `handleListChannels` | Channel-visibility filtering implemented ~4× with comments instructing they "must mirror" each other. The recent private-channel fixes (e.g. `2bfe6d6`) show this is actively churning — a drift between copies is an information-disclosure bug waiting to happen. | Extract a single `VisibleChannels(userID)` (natural home: `service.PermissionService` or the existing `permissions.Checker`), consume it from all four sites, and add a test asserting REST and WS agree. | M |
| A-2026-07-08 | MEDIUM | Protocol integrity | `Server/ws/message_types.go` header comment; `Client/…/src/lib/protocolTypes.ts` header + "Extensions (not in protocol-schema.json…)" comments | Both sides claim `docs/protocol-schema.json` is the generated single source of truth. The file does not exist; the two constant sets are maintained by hand and have already grown divergent "extension" entries. | Either commit a real `protocol-schema.json` + generator (best: also emits protocol.md tables), or delete the claim and add a cross-language equality test over the two constant sets. | M |
| A-2026-07-09 | MEDIUM | Real-time | `Server/ws/handlers.go` (`handleMessage` V2-then-V1 fallback), dual registration in `NewHub` (`Server/ws/hub.go`) | Strangler-fig V1+V2 dispatch is live: two parsers (lenient/strict), two registries, per-type duplication. | Finish the migration: port remaining V1 types to V2, then delete the V1 path. Track remaining types in an issue so the count visibly shrinks. | M/L |
| A-2026-07-10 | MEDIUM | Composition | `Server/api/router.go:34` (`NewRouter`, ~278 lines) | God-constructor builds rate limiter, TOTP key, storage, services, hub, LiveKit client+process, updater, admin + plugin handlers; spawns goroutines; returns a cleanup closure covering only one of them. Hard to test wiring in isolation; lifecycle ownership is implicit. | Split construction (a `Deps`/`App` struct built in `main.go`) from route mounting (`NewRouter(deps)`); return a composite `io.Closer`. | M |
| A-2026-07-11 | MEDIUM | Real-time | `Server/ws/hub.go` (`SetLiveKit`, `SetEventPersister`, `SetPluginRegistry`, …) | Hub is a mega-object wired post-construction via setters that "must be called before Run" — temporal coupling; a missed setter is a nil-deref at runtime, not a compile error. | Move required collaborators into `NewHub` params (or an options struct validated before `Run`). Full Hub decomposition is a separate, larger effort (backlog 12). | S (constructor) / L (decomposition) |
| — | MEDIUM | Layering | `Server/ws/hub.go:182` (`refreshSettingsLocked`) | Hub runs inline `SELECT value FROM settings WHERE key='server_name'` instead of using `SettingsStore` — the only raw SQL in the real-time layer. | Route through the store; folds into A-2026-07-06 but is a 20-minute standalone fix. | S |
| — | LOW | Scaling posture | `Server/auth/ratelimit.go` (documented), in-memory pub/sub + ring buffer, process-local TOTP replay | Single-instance coupling is structural and *documented* — this is a deliberate design, not a bug. Recorded here so the constraint stays visible ([architecture/system-overview.md D8](architecture/system-overview.md)). | No action now; revisit only if multi-instance ever becomes a goal. | — |
| A-2026-07-13 | LOW | Schema hygiene | `sounds` table (`Server/migrations/001`), `audit_log` + `audit_log_v6` (`003`) | Dead/duplicated schema: soundboard was removed but its table remains; two audit-log tables coexist after the 003 rebuild. | Add a cleanup migration (drop `sounds`, finish the audit_log consolidation) next time a migration ships anyway. | S |
## 4. Client architecture findings
Verdict: the client's fundamentals are strong — immutable store discipline,
TOFU pinning in Rust, keychain credentials with IPC redaction, generation
counters against stale listeners, and an unusually deep test/tooling stack
(Vitest, dual Playwright suites, Stryker, oxlint + type-checked ESLint, Knip).
Findings target structure and one security gap.
| ID | Sev | Area | Evidence | Finding | Recommendation | Effort |
|----|-----|------|----------|---------|----------------|--------|
| A-2026-07-02 | HIGH | Security | `src/main.ts` (`allowSelfSigned: true` at API-client construction); `src-tauri` http plugin built with `dangerous-settings` | Every REST call accepts any certificate. The WS and LiveKit paths pin TOFU fingerprints in Rust; the HTTP path — which carries the auth token on every request — does not. An active MITM can capture tokens without triggering the cert-mismatch modal. | Implement the acknowledged fix: a TOFU HTTP proxy in Rust (mirror `ws_proxy.rs`), or route REST through a pinned Rust command. Until then this is the client's weakest transport link. | M |
| A-2026-07-12 | MEDIUM | Coherence | `src/components/solid/` (154 LOC), `src/lib/solidMount.ts`, `src/lib/solidAdapter.ts`, `vite-plugin-solid` config; CHANGELOG "Solid.js migration (abandoned)" | The abandoned migration's beachhead, adapters, build plugin, and test deps remain, and `docs/client-architecture.md` (2026-03-30) still describes a SolidJS client. Two mental models for contributors, one of them false. | Delete the beachhead + adapters + build plugin; replace `client-architecture.md` content with a pointer to [architecture/client.md](architecture/client.md) or a rewrite. | S |
| — | MEDIUM | Maintainability | `src/lib/livekitSession.ts` (1,719 LOC); `AccountTab.ts` (845), `SidebarArea.ts` (812), `LoginForm.ts` (699) | `livekitSession.ts` owns the connection state machine, E2EE, track management, reconnection, and diagnostics in one class. It is the highest-risk file to modify in the client. | Extract E2EE (already has `e2eeCrypto.ts` as a seam) and track management into collaborators; keep the state machine as the core. Settle for splitting the settings tabs opportunistically. | M |
| — | MEDIUM | State | `src/stores/voice.store.ts` imports members/auth stores; `auth.store.clearAuth()` reaches into `leaveVoice()` + notification cleanup; business logic in `main.ts` subscribers | Cross-store singleton coupling: teardown ordering lives implicitly in import graphs and bootstrap subscribers. | Introduce a thin session-lifecycle module (login/logout orchestration) that calls stores, so stores stop calling each other. | M |
| A-2026-07-14 | LOW | Hygiene | `#5865F2` literal ×18 across 11 files (`main.ts`, `ServerPanel.ts`, `DmSidebar.ts`, `MemberPickerModal.ts`, `SidebarArea.ts`, …); `localhost:8443` ×3; 64 `setTimeout`/`setInterval` sites; 12 `innerHTML` uses | Scattered magic values and manual timer lifecycles; `src/lib/constants.ts` holds a single constant. | Centralize into constants/tokens; adopt a tiny `managedTimer(destroyScope)` helper so `destroy()` paths can't leak intervals. | S |
| — | LOW | Error handling | `catch {}` swallows in `preferences.ts`, `ws.ts` unsubscribe cleanup, `disconnectProxy`; widespread `void`-prefixed fire-and-forget | Mostly deliberate (per lint config), but a handful of swallows hide real failures (e.g. preference persistence silently failing). | Log-at-debug in the swallow sites; keep the pattern otherwise. | S |
## 5. Process & CI findings
| ID | Sev | Evidence | Finding | Recommendation | Effort |
|----|-----|----------|---------|----------------|--------|
| A-2026-07-04 | HIGH | `.github/workflows/ci.yml``client-tests` job annotated "KNOWN RED pending reboot-plan P2 triage"; no Playwright job | The Go side is gated hard (race, deadlock tag, govulncheck, golangci-lint, sqlc-verify, 4-tag build matrix) but the client's 157-file test suite is red and non-blocking, and E2E never runs in CI. For an AI-first workflow where "quality [is] validated primarily through automated checks" (README), the client half of that promise is currently unenforced. | Triage the red suite to green, flip `client-tests` to blocking, then add at least the web Playwright suite as a nightly non-blocking job (prior #11) before promoting it to a gate. | M |
| A-2026-07-03 | HIGH | `git log` on `docs/api.md`, `protocol.md`, `schema.md` (all 2026-04-02) vs code churn through 2026-07-19 | No process keeps the reference specs current — the July burst (events table, plugins, E2EE, private channels) shipped without touching them. | After the one-time refresh (backlog 4), add a PR-checklist line (mirroring the blueprint maintenance rule in [architecture/README.md](architecture/README.md)): protocol/API/schema changes update the matching spec in the same PR. | S |
| A-2026-07-15 | LOW | `docs/plans/security-hardening-remediation.md` W1-3/W3-5 reference `Server/store/postgres.go` (deleted) | The remediation plan predates the Postgres removal; two waves partially target dead code. | Annotate the affected items rather than rewriting the plan. | S |
| — | LOW | `ci.yml` `tauri-build` job: `if: github.event_name == 'pull_request' && github.base_ref == 'main'` | Full client build (incl. Clippy `-D warnings`, cargo audit) never runs on push to main — a merge that breaks the native build is caught only at the next PR. | Add a push-to-main trigger for `tauri-build` (or a nightly). | S |
## 6. Prioritized improvement backlog
Ranked by severity × effort; quick wins float within tier. S/M/L ≈ hours / days / week+.
| # | Item | Finding | Sev | Effort |
|---|------|---------|-----|--------|
| 1 | Decide + resolve the `announcement` channel-type contradiction (implement or strip from specs/admin) | A-2026-07-01 | HIGH | S (decision) |
| 2 | Delete (or finally adopt) the dead `db/dbgen` sqlc layer; adjust the `sqlc-verify` CI job to match | A-2026-07-05 | MEDIUM | S |
| 3 | Extract the single channel-visibility function replacing the 4 "must mirror" copies; add REST/WS agreement test | A-2026-07-07 | MEDIUM | M |
| 4 | One-PR refresh of api.md / protocol.md / schema.md against §2, incl. E2EE + plugins + migrations 009015; then enforce spec-updates-with-code via PR checklist | A-2026-07-03 | HIGH | M |
| 5 | Execute the store-layer decision from prior #6: remove `Server/store/` (P4 plan) or test it directly | prior #6 | HIGH | M |
| 6 | Client HTTP TOFU pinning (Rust proxy mirroring `ws_proxy.rs`) | A-2026-07-02 | HIGH | M |
| 7 | Stop discarding `LogAudit` errors in `admin/handlers_backup.go`; fix the contradictory upload `Cache-Control` | prior #10, W3-4 | MEDIUM | S |
| 8 | Remove the SolidJS beachhead + adapters; retire or rewrite `docs/client-architecture.md` | A-2026-07-12 | MEDIUM | S |
| 9 | Resolve the `protocol-schema.json` ghost: real codegen or an equality test between Go and TS constant sets | A-2026-07-08 | MEDIUM | M |
| 10 | Green + blocking client unit suite; nightly Playwright | A-2026-07-04 | HIGH | M |
| 11 | Finish the V2 dispatch migration; delete V1 | A-2026-07-09 | MEDIUM | M/L |
| 12 | Consolidate DB access behind the service layer (start with auth routes, prior #9); then Hub constructor cleanup and decomposition | A-2026-07-06, -10, -11 | MEDIUM | L |
---
*Blueprints referenced throughout live in [docs/architecture/](architecture/README.md);
update a diagram and its doc in the same PR as any structural change to its
source-of-truth files. Line-number evidence in this report is a snapshot of
commit `ddc49f0`.*