docs: update specs for LiveKit migration (Phase 4)

- CLAUDE.md: update key features (LiveKit voice/video), remove
  WebRTC track ID rules, update project structure
- PROTOCOL.md: replace voice_offer/answer/ice with voice_token,
  remove soundboard_play, note client-side speaker detection
- CHATSERVER.md: replace Pion SFU with LiveKit companion process,
  update architecture, security, and library references
This commit is contained in:
jevb
2026-03-20 05:50:14 +01:00
parent f1011a881a
commit 620fac778d
2 changed files with 705 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
# ChatServer — Self-Hosted Windows Chat Platform
Native Windows desktop client + self-hosted server.
Two executables: `chatserver.exe` (server) and
`OwnCord.exe` (Tauri v2 client). Server operator runs
the server, friends install the client.
## Tech Stack
### Server (`chatserver.exe`)
- **Go** — Single exe, no dependencies. Embeds admin web UI via `go embed`.
- **SQLite** — Single `.db` file. WAL mode. Zero config.
- **LiveKit** — Voice/video media server. Runs as a companion
process alongside `chatserver.exe`. Token-based auth.
- **Admin panel** — Web-based, served at `/admin`.
Browser access, not part of the client.
### Client (`OwnCord.exe`)
**Tauri v2** (Rust backend + TypeScript/HTML/CSS frontend).
See LANGUAGE-REVIEW.md for the evaluation that led to this
choice, and CLIENT-ARCHITECTURE.md for the full design.
- Tauri v2 desktop app using system WebView2 (NOT Electron)
- ~10-15 MB install size, ~30-50 MB RAM idle
- TypeScript frontend with CSS from HTML mockups
- WebSocket client for real-time chat (browser `WebSocket` API)
- LiveKit for voice/video (LiveKit JS SDK in webview)
- Global keyboard hooks via `tauri-plugin-global-shortcut`
- System tray via Tauri's built-in tray support
- Windows toast notifications via `tauri-plugin-notification`
- Windows Credential Manager via `windows-rs` Rust crate
- NSIS installer via Tauri bundler
## Architecture
```text
SERVER (chatserver.exe) — runs on the host machine
├── REST API (Go net/http)
├── WebSocket Hub (real-time messages, presence, typing)
├── LiveKit Companion Process (voice/video media)
├── SQLite Database (data/chatserver.db)
├── File Storage (data/uploads/)
├── Admin Web UI (embedded, browser-based, /admin)
└── config.yaml
CLIENT (OwnCord.exe) — installed by each friend
├── Native Windows UI
├── WebSocket Client (chat connection)
├── LiveKit Client (voice/video via livekitSession.ts)
├── Audio Engine (device management, noise suppression)
├── Local Settings (connection profiles, keybinds, audio config)
└── System Tray Integration
```
### How It Works
1. Server operator runs `chatserver.exe` on their PC/home server
2. Friends download and install `OwnCord.exe`
3. Client connects to the server via IP/domain + port
4. All chat, voice, video, and file transfers go through the server
5. Admin manages the server through a browser at `https://server-ip:port/admin`
---
## Phase 1: Protocol & Server Core (23 weeks)
- [ ] Define client-server protocol over WebSocket
(JSON messages with type/payload structure)
- [ ] Message types: auth, chat, typing, presence,
channel_update, voice_signal, file_transfer
- [ ] Server: Go project with `go embed` for admin
panel static files only
- [ ] SQLite setup with migrations on startup (users,
channels, messages, sessions, roles, invites)
- [ ] config.yaml generation on first run (port, name,
max upload size, voice quality, TLS mode)
- [ ] Server systray icon (getlantern/systray) — minimize to tray, status
indicator, open admin panel, quit
- [ ] Windows Firewall handling on first launch
- [ ] Optional: register as Windows Service for headless operation
## Phase 2: Auth & Security (23 weeks)
- [ ] Invite-only registration — server generates
invite codes, client has "Redeem Invite" flow
- [ ] bcrypt (cost 12+) passwords, server-side session tokens (256-bit random)
- [ ] Client stores auth token securely via Windows Credential Manager / DPAPI
- [ ] Login rate limiting: 5 attempts/min/IP, lockout after 10 failures
- [ ] Optional TOTP 2FA (`pquerna/otp`) — QR code
during setup, prompts on login
- [ ] Roles: Owner, Admin, Moderator, Member + custom roles with bitfield permissions
- [ ] Per-channel permission overrides, enforced server-side on every action
- [ ] TLS modes: self-signed (default), Let's Encrypt,
manual cert, off (Tailscale)
- [ ] Client: certificate pinning or trust-on-first-use (TOFU) for self-signed certs
## Phase 3: Client App — Core UI (34 weeks)
- [ ] Connection dialog: server address, port, login/register, invite code entry
- [ ] Save server profiles (connect to multiple
servers like TeamSpeak)
- [ ] Main window layout: server list → channel list → message area → member list
- [ ] Channel tree view with categories, text channels, voice channels
- [ ] Message rendering: markdown, code blocks, timestamps, avatars, replies, reactions
- [ ] Message input: multi-line, markdown preview, emoji picker, file drag-and-drop
- [ ] Unread indicators, @mention badges per channel
- [ ] System tray: minimize to tray, notification popups, badge count
- [ ] Keyboard shortcuts: Ctrl+K quick switcher,
Escape to close panels, customizable PTT key
- [ ] Settings: account, appearance (light/dark),
notifications, audio devices, keybinds
## Phase 4: Real-Time Chat Features (23 weeks)
- [ ] WebSocket client with auto-reconnect, exponential
backoff, message replay on reconnect
- [ ] Send/receive messages in real-time, append to scrollback
- [ ] Message history: paginated from server on channel switch, scroll-to-load-more
- [ ] Threads, replies (inline preview), reactions (emoji), edit, delete
- [ ] Typing indicators ("X is typing..." below input)
- [ ] Online/offline/idle/DnD presence with status icons in member list
- [ ] File uploads: drag-and-drop or clipboard paste,
progress bar, inline image previews
- [ ] Client-side file validation before upload (size check, warn on large files)
- [ ] Search: query server FTS5 endpoint, display results with jump-to-message
- [ ] Windows toast notifications with action buttons (reply, mark read)
- [ ] Notification sounds (configurable, per-channel mute/override)
## Phase 5: Voice & Video (35 weeks)
- [ ] LiveKit integration in native client for voice/video
(client connects to LiveKit directly using token from server)
- [ ] Audio device selection: input/output dropdowns in settings, live preview
- [ ] Voice channels: click to join/leave, show connected users with speaking indicators
- [ ] Voice controls: mute (button + keybind), deafen, per-user volume sliders
- [ ] Push-to-talk: configurable global hotkey that works in fullscreen games
- [ ] Voice activity detection with configurable sensitivity
- [ ] Noise suppression (RNNoise or equivalent, bundled with client)
- [ ] Server-side: LiveKit companion process with token-based auth
and webhook sync for voice state updates
- [ ] Voice quality: low (32kbps) / medium (64kbps) / high (128kbps Opus)
- [ ] Screen sharing via LiveKit screen share track
- [ ] Video calls: camera capture, displayed in voice channel panel
## Phase 6: Admin Panel — Web-Based (12 weeks)
- [ ] Served by server at `/admin`, browser-only access
- [ ] Auth: admin credentials, session-based
- [ ] Dashboard: connected users, message count, disk usage, CPU/RAM, uptime
- [ ] User management: list all, edit roles, ban/unban, reset password, force disconnect
- [ ] Channel management: create, rename, reorder, set permissions, archive
- [ ] Invite management: generate, view active, set expiry/use limit, revoke
- [ ] Server settings: name, icon, MOTD, max upload size, voice quality, TLS config
- [ ] Moderation: kick, ban, temp ban, slow mode, mute, word filter, audit log
- [ ] Backup: trigger manual backup, configure
schedule, view/restore from admin panel
- [ ] Built with simple HTML/CSS/JS embedded in the server binary
## Phase 7: Distribution & Updates (12 weeks)
- [ ] **Server:** GitHub Actions builds
`chatserver.exe` (amd64), SHA256, GitHub Release
- [ ] **Client:** Tauri bundler (NSIS) installer —
Program Files, Start Menu, auto-start, protocol
handler for `chatserver://` invite links
- [ ] Client auto-update: check GitHub releases on
launch, prompt to download + install
- [ ] Server update: admin panel shows available update, one-click download + restart
- [ ] Docs: Quick Start, Port Forwarding, Tailscale,
Client install guide
- [ ] Security hardening checklist for server operators
- [ ] SECURITY.md, README.md, CONTRIBUTING.md
---
## Windows-Specific Details
### Client (Tauri v2)
- **Installer:** Tauri bundler (NSIS, ~10-15 MB).
Registers `chatserver://` protocol handler.
- **Auto-start:** Registry key
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`.
- **Credentials:** Auth tokens stored in Windows
Credential Manager via `windows-rs` Rust crate.
- **Push-to-talk:** Global hotkey via
`tauri-plugin-global-shortcut`.
- **Audio:** LiveKit SDK via `livekitSession.ts`.
- **Screen capture:** LiveKit screen share track.
- **Notifications:** `tauri-plugin-notification`
(Windows toast).
- **Tray:** Tauri built-in system tray with badge.
- See CLIENT-ARCHITECTURE.md for full design.
### Server
- **Firewall:** Prompt on first run. Installer can pre-register firewall rule.
- **SmartScreen:** Unsigned exe shows warning. Code signing cert resolves this.
- **Data path:** `data/` next to exe. Installer version uses `%APPDATA%/ChatServer/`.
- **Logs:** `data/logs/` with daily rotation, viewable from admin panel.
- **Service mode:** `chatserver.exe --service install` to register as Windows Service.
## Security Priorities
**Critical:** Invite-only registration, bcrypt auth,
TLS (self-signed minimum), file upload validation
(magic bytes, block executables), input sanitization
server-side, credential storage via DPAPI, backups.
**High:** Rate limiting, TOTP 2FA, role permissions,
WebSocket auth, LiveKit token auth, cert pinning/TOFU,
update integrity (SHA256).
## Server Libraries (Go)
| Purpose | Library |
| --- | --- |
| HTTP/routing | `net/http` + `chi` |
| WebSocket | `nhooyr.io/websocket` |
| LiveKit | `livekit/server-sdk-go` (token generation, webhook validation) |
| SQLite | `modernc.org/sqlite` (pure Go) |
| Auth | `golang.org/x/crypto/bcrypt` |
| TOTP | `pquerna/otp` |
| Sanitization | `bluemonday` |
| TLS | `golang.org/x/crypto/acme/autocert` |
| Systray | `getlantern/systray` |
| Config | `koanf` |
| Logging | `log/slog` |
+475
View File
@@ -0,0 +1,475 @@
# WebSocket Protocol Spec
All client-server communication (except file uploads and
admin panel) happens over a single WebSocket connection.
Messages are JSON with a `type` and `payload`.
## Message Format
```json
{
"type": "message_type",
"id": "unique-request-id",
"payload": { }
}
```
- `type` — string, required. Determines how payload is interpreted.
- `id` — string, optional. Client-generated UUID for request/response correlation.
- `payload` — object, required. Contents vary by type.
Server responses to client requests include the same `id` for correlation.
---
## Authentication
### Client → Server
```json
{ "type": "auth", "payload": { "token": "session-token-here" } }
```
### Server → Client (success)
```json
{
"type": "auth_ok",
"payload": {
"user": {
"id": 1, "username": "alex",
"avatar": "uuid.png", "role": "admin"
},
"server_name": "My Server",
"motd": "Welcome!"
}
}
```
### Server → Client (failure)
```json
{ "type": "auth_error", "payload": { "message": "Invalid or expired token" } }
```
Connection is closed by server after auth_error.
---
## Chat Messages
### Send Message (Client → Server)
```json
{
"type": "chat_send",
"id": "req-uuid",
"payload": {
"channel_id": 5,
"content": "Hello everyone!",
"reply_to": null,
"attachments": ["upload-uuid-1"]
}
}
```
### Message Broadcast (Server → Client)
```json
{
"type": "chat_message",
"payload": {
"id": 1042, "channel_id": 5,
"user": {
"id": 1, "username": "alex",
"avatar": "uuid.png"
},
"content": "Hello everyone!",
"reply_to": null,
"attachments": [{
"id": "upload-uuid-1",
"filename": "photo.jpg",
"size": 204800,
"mime": "image/jpeg",
"url": "/files/upload-uuid-1"
}],
"timestamp": "2026-03-14T10:30:00Z"
}
}
```
### Send Ack (Server → Client)
```json
{
"type": "chat_send_ok",
"id": "req-uuid",
"payload": {
"message_id": 1042,
"timestamp": "2026-03-14T10:30:00Z"
}
}
```
### Edit Message (Client → Server)
```json
{
"type": "chat_edit",
"id": "req-uuid",
"payload": {
"message_id": 1042,
"content": "Hello everyone! (edited)"
}
}
```
### Edit Broadcast (Server → Client)
```json
{
"type": "chat_edited",
"payload": {
"message_id": 1042,
"channel_id": 5,
"content": "Hello everyone! (edited)",
"edited_at": "2026-03-14T10:31:00Z"
}
}
```
### Delete Message (Client → Server)
```json
{ "type": "chat_delete", "id": "req-uuid", "payload": { "message_id": 1042 } }
```
### Delete Broadcast (Server → Client)
```json
{ "type": "chat_deleted", "payload": { "message_id": 1042, "channel_id": 5 } }
```
### Reaction Add/Remove (Client → Server)
```json
{ "type": "reaction_add", "payload": { "message_id": 1042, "emoji": "👍" } }
{ "type": "reaction_remove", "payload": { "message_id": 1042, "emoji": "👍" } }
```
### Reaction Broadcast (Server → Client)
```json
{
"type": "reaction_update",
"payload": {
"message_id": 1042,
"channel_id": 5,
"emoji": "👍",
"user_id": 1,
"action": "add"
}
}
```
---
## Typing Indicators
### Client → Server (throttle to 1 per 3 seconds)
```json
{ "type": "typing_start", "payload": { "channel_id": 5 } }
```
### Server → Client (broadcast to channel members)
```json
{
"type": "typing",
"payload": {
"channel_id": 5,
"user_id": 1,
"username": "alex"
}
}
```
Client-side: show indicator for 5 seconds, reset on new typing event from same user.
---
## Presence
### Presence Client → Server
```json
{ "type": "presence_update", "payload": { "status": "online" } }
```
Status values: `online`, `idle`, `dnd`, `offline`
### Presence Server → Client (broadcast)
```json
{ "type": "presence", "payload": { "user_id": 1, "status": "online" } }
```
Server auto-sets `idle` after 10 minutes of no WebSocket activity.
---
## Channel Updates
### Server → Client (on channel created/edited/deleted/reordered)
```json
{
"type": "channel_create",
"payload": {
"id": 8, "name": "gaming",
"type": "text",
"category": "Hangout", "position": 3
}
}
{
"type": "channel_update",
"payload": {
"id": 8, "name": "gaming-talk",
"position": 4
}
}
{ "type": "channel_delete", "payload": { "id": 8 } }
```
Channel types: `text`, `voice`, `announcement`
---
## Voice Signaling
### Join Voice Channel (Client → Server)
```json
{ "type": "voice_join", "payload": { "channel_id": 10 } }
```
### Server → Client (voice state updates, broadcast to channel)
```json
{
"type": "voice_state",
"payload": {
"channel_id": 10, "user_id": 1,
"username": "alex",
"muted": false, "deafened": false,
"speaking": false,
"camera": false, "screenshare": false
}
}
```
### Voice User Left (Server → Client)
```json
{ "type": "voice_leave", "payload": { "channel_id": 10, "user_id": 1 } }
```
### voice_token (Server → Client)
Sent after a successful `voice_join`. Contains the LiveKit access token
and server URL for the client to connect directly to LiveKit.
```json
{
"type": "voice_token",
"payload": {
"channel_id": 10,
"token": "eyJhbGciOi...",
"url": "ws://localhost:7880"
}
}
```
- `channel_id` (number) — the voice channel joined
- `token` (string) — LiveKit JWT access token
- `url` (string) — LiveKit WebSocket URL (e.g. `ws://localhost:7880`)
### Voice Config (Server → Client, sent after voice_join acceptance)
```json
{
"type": "voice_config",
"payload": {
"channel_id": 10, "quality": "medium", "bitrate": 64000,
"max_users": 50
}
}
```
Client uses `bitrate` to configure the Opus encoder. Other fields are
informational for UI. (`threshold_mode` and `top_speakers` fields have
been removed — LiveKit handles audio mixing and speaker selection
internally.)
### Voice Control (Client → Server)
```json
{ "type": "voice_mute", "payload": { "muted": true } }
{ "type": "voice_deafen", "payload": { "deafened": true } }
```
### Voice Camera / Screenshare (Client → Server)
```json
{ "type": "voice_camera", "payload": { "enabled": true } }
{ "type": "voice_screenshare", "payload": { "enabled": true } }
```
Requires `USE_VIDEO` (bit 11) or `SHARE_SCREEN` (bit 12) permission.
Rate limit: 2/sec per user.
**Note:** Active speaker detection (`voice_speakers`) is no longer a
server→client WebSocket message. Speaker detection is handled client-side
via LiveKit SDK events (`ParticipantEvent.IsSpeakingChanged`).
---
## Member Updates
### Server → Client
```json
{
"type": "member_join",
"payload": {
"user": {
"id": 5, "username": "newuser",
"avatar": null, "role": "member"
}
}
}
{ "type": "member_leave", "payload": { "user_id": 5 } }
{ "type": "member_update", "payload": { "user_id": 5, "role": "moderator" } }
{ "type": "member_ban", "payload": { "user_id": 5 } }
```
---
## Server Restart
### Restart Server → Client
```json
{
"type": "server_restart",
"payload": {
"reason": "update",
"delay_seconds": 5
}
}
```
- `reason` (string): Why the server is restarting. Currently only `"update"`.
- `delay_seconds` (integer): How many seconds until the server shuts down.
Client behavior: Display a banner ("Server restarting..."),
then auto-reconnect after the delay expires.
---
## Initial State (sent after auth_ok)
### Ready Server → Client
```json
{
"type": "ready",
"payload": {
"channels": [
{
"id": 1, "name": "general",
"type": "text", "category": "Main",
"position": 0, "unread_count": 3,
"last_message_id": 1040
},
{
"id": 10, "name": "voice-chat",
"type": "voice", "category": "Main",
"position": 1
}
],
"members": [
{
"id": 1, "username": "alex",
"avatar": "uuid.png",
"role": "admin", "status": "online"
},
{
"id": 2, "username": "jordan",
"avatar": null,
"role": "member", "status": "idle"
}
],
"voice_states": [
{ "channel_id": 10, "user_id": 2, "muted": false, "deafened": false }
],
"roles": [
{
"id": 1, "name": "Owner",
"color": "#E74C3C",
"permissions": 2147483647
},
{
"id": 2, "name": "Admin",
"color": "#F39C12",
"permissions": 1073741823
},
{ "id": 3, "name": "Member", "color": null, "permissions": 1049601 }
]
}
}
```
---
## Message History (REST, not WebSocket)
Fetched via REST API, not WebSocket, to keep the WS connection lean.
```text
GET /api/channels/{id}/messages?before={msg_id}&limit=50
```
---
## Error Format
Any request that fails returns:
```json
{
"type": "error",
"id": "original-req-uuid",
"payload": {
"code": "FORBIDDEN",
"message": "No permission to post here"
}
}
```
Error codes: `FORBIDDEN`, `NOT_FOUND`, `RATE_LIMITED`, `INVALID_INPUT`,
`SERVER_ERROR`, `CHANNEL_FULL`, `VOICE_ERROR`
---
## Rate Limits
- Chat messages: 10/sec per user
- Typing events: 1/3sec per user per channel
- Presence updates: 1/10sec per user
- Reactions: 5/sec per user
- Voice signaling: 20/sec per user
- Voice camera/screenshare: 2/sec per user
Server sends `rate_limited` error with `retry_after` in seconds.