* test(b3-6): seeded hub simulation and fault-injected transport (items 2 and 3) Server/ws/hub_sim_test.go drives a PCG-seeded interleaving of subscribe, broadcast (global, channel, recipients-scoped, sequenced DM), ack, disconnect and reconnect-transfer over a real Hub with eight headless clients, and a model client checks the per-client FIFO/seq oracle from Server/CLAUDE.md after every step: strictly increasing seq per connection, exact audience delivery (nothing lost, extra or twice), a resume replayed exactly from the watermark to the seq at which registerNow ran, h.seq advancing only for a frame that reached the ring, an evicted watermark refused a replay, and a replaced socket's late teardown reporting replaced=true. The resume step runs reconnectRegister as-is (snapshot and registerNow under one seqMu section) on a goroutine while up to three broadcasts race it; the model recovers the snapshot point from the replay burst, so any interleaving is checkable. OWNCORD_SIM_SEED replays one seed, OWNCORD_SIM_SEEDS (default 20) and OWNCORD_SIM_STEPS (default 200) size a run, and a failure prints the seed, the step, a ready-to-paste replay line and the last steps. The default runs in about 2.3 s under -race; `make sim` runs 10,000 steps per seed. Server/ws/faultconn_test.go is the seeded, deterministic frame transport the simulation reads through: drop, tail cut, duplicate, bounded reorder and an order-preserving lag from its own PCG stream, exported to ws_test through export_test.go as NewFaultConnForTest. The simulation's default wire is a lag plus tail cuts, the one fault a TCP-backed WebSocket really has; the silent drop is the negative control that proves the oracle notices a lost replay. BenchmarkReconnectStorm resumes 50 live clients per op through the same path. newTestHub and its three seed helpers take testing.TB so the benchmark can share them. No production code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * docs(b3-6): evidence block for items 2 and 3 (hub simulation, fault transport) Oracle, the RED/GREEN excerpts (inverted assertion, seed replay, drop-all wire, unsynchronized registerNow), wall-clock and benchmark figures, gate results and the epoch-harness decision, under B3-6 in the plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * fix(b3-6): hub sim — deterministic topic limiter for exact replay, a floor on the step mix, auth-frame-wins under transfer, wire-seed mixing Review fixes for items 2 and 3. Exact replay. TopicRateLimiter keys its window on time.Now(), so at 10,000 steps the shed boundary was a timing-dependent step and every later seq differed between runs; the printed OWNCORD_SIM_SEED line could not reproduce a failure. FreezeTopicLimiterForTest (export_test.go) swaps the hub's limiter for one whose window never rolls over inside a run, so the shed is a per-channel count. Three more leaks of the scheduler's interleaving into the trajectory surfaced once that was fixed, and are closed the same way — by taking the decision away from the race or making both outcomes read the same: racing frames are pulled into the wire at attach time (queue fill no longer depends on which side of the snapshot they fell), the racing burst is aimed at the resuming client's own audience (a replay-superset frame was read iff it landed before the snapshot), and a resume within the burst's reach of the ring's eviction boundary is not raced (the allocations could evict the watermark before or after the snapshot and pick replay or fallback). Three runs of one seed now print byte-identical stats; what still varies — how many racing seqs land in the replay burst — is printed on its own line and stated in the doc comment. Floor. TestHubSimulation aggregates the per-seed stats and requires every load-bearing transition (the four broadcast kinds, resume, fallback, fresh, cut, kicked, racing-in-replay) at least once across the default run, so a constant change cannot turn the simulation into no-ops with CI green. Its first run found that the overflow kick had become unreachable at 200 steps; the sim's queue is 12 now (production stays 256). Also: the resume step draws active_channel_id as none / the open channel / another channel whether or not the old socket is registered, so registerNow's auth-frame-wins branch runs under the transfer; the wire's PCG takes the seed and (idx<<32|conns) as its two words instead of an arithmetic mix that collided past 131 connections; seedTestUser takes testing.TB like its siblings; the evidence block lists what the simulation does not cover and the -timeout 60m the ten-pass deadlock gate needs, with the same line under Traps carried forward. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * test(b3-6): hub sim — floor on raced resumes instead of the scheduler-decided key; log when the floor is skipped racing-in-replay was the one floor key the scheduler decides, so a correct hub could in principle fail the floor on a run where no racing seq landed inside a burst. The floor now keys on raced resumes — a resume that got a replay while a burst ran (burst > 0 && ok), which the seed determines — and racing-in-replay stays a printed count. The floor also says so when it is skipped for OWNCORD_SIM_SEED or a shorter seed list instead of returning silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo * test(b3-6): hub sim — raced counts a broadcast allocated while registration was in progress, not the requested burst Codex P2 on #1458: raced incremented on burst > 0, which counted a resume whose goroutine had returned before the first broadcast ran and one whose every channel broadcast the limiter shed, so the floor could pass with no broadcast overlapping a registration. raced now counts a resume where a racing broadcast allocated a seq while the reconnect goroutine had not yet been observed to return (the driver's done handshake, checked after each allocation). That is the scheduler's call, so raced moves off the deterministic stats line and is floored only in aggregate across the 20 default seeds — 179 bursts per run, 178–179 observed overlapping in three measured runs, odds named in the comment and the evidence block. The requested burst stays a printed, seed-determined count (bursts) and is floored as before; the floor logs its totals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmiqjgTuov1stBTB6uGkvo --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
OwnCord
A self-hosted chat app I build for me and my friends — text channels, voice and video, and a server you actually own.
Alpha, and a hobby project. This is something I build for fun and run for a small group of friends. It isn't a product, it comes with no support commitment, and it isn't production-ready. Expect rough edges, rapid changes, and the occasional breaking change.
Don't use it for anything sensitive.
It's a Go server plus a Tauri desktop client: real-time messaging, voice/video via LiveKit, file sharing, and a web admin panel. Run the server on a spare box or a VPS, hand your friends an invite code, and that's the whole thing.
How it's built
Most of the implementation is generated with AI tooling, with quality held up by automated checks — CI, tests, linting — and by me and my friends actually using it. That keeps iteration fast, and it also means behaviour can change quickly between releases.
What works right now
| Area | Status |
|---|---|
| Core chat flow | Working in alpha |
| Voice/video | Working in alpha |
| Admin panel | Working in alpha |
| Security hardening | Ongoing review passes; findings and their statuses are tracked in the dated audits in docs/ (see the Docs Index below) |
Platform Support (Current Releases)
| Component | Windows x64 | Linux x64 | Linux ARM64 |
|---|---|---|---|
| Server binary | Yes | Yes | Not yet |
| Desktop client | Yes (NSIS installer) | Yes (AppImage, .deb) | Yes (AppImage, .deb) |
| Docker server | N/A | Build from source (compose) | Not yet |
Start Here
- New user quick path: docs/quick-start.md
- Linux Docker deployment: docs/deployment.md
- Remote access without router config: docs/tailscale.md
- Manual router/network setup: docs/port-forwarding.md
Quick Start
Option A: Prebuilt binaries
- Download assets from Releases (binaries, checksums, signatures, and a full source snapshot per release).
- Run the server binary:
- Windows:
chatserver.exe - Linux:
./chatserver
- Windows:
- Open
https://localhost:8443/adminand complete the setup wizard — it creates your Owner account and configures the server for you (settings are saved toconfig.yamlautomatically). - Generate invite codes in the admin panel and share them with friends.
Option B: Docker (Linux server)
cd Server
cp .env.example .env
cp livekit.yaml.example livekit.yaml
# Edit both files before starting
docker compose up -d
See the full setup guide in docs/deployment.md.
The client uses TOFU (Trust On First Use) for self-signed certificates: it prompts once, then pins the certificate for future connections.
What OwnCord Already Has
- Real-time channels and direct messages over WebSocket
- Voice/video channels via LiveKit — the LiveKit server binary is downloaded and managed for you
- Invite-only registration and role-based permissions
- Web admin panel with logs, backups, and update tooling
- File uploads and inline media rendering
- TOTP 2FA support and API rate limiting
- Desktop client auto-update with signature verification
- WASM plugin system (slash commands; sandboxed, default-disabled — enable via
plugins.enabledand build with-tags wazero) - GIF picker — off by default; each server supplies its own
Klipy key via
gif.api_key(setup)
See deeper feature and architecture docs in docs/architecture/ and docs/protocol.md.
Architecture
Two main components:
- Go server (REST API, WebSocket hub, SQLite, admin panel)
- Tauri v2 desktop client (Rust backend + TypeScript frontend)
+---------------------+ +---------------------+
| OwnCord Client | | OwnCord Server |
| (Tauri v2) | | (Go) |
| | | |
| +---------------+ | WSS | +---------------+ |
| | Chat UI |--+------->| | WebSocket Hub| |
| +---------------+ | | +---------------+ |
| +---------------+ | HTTPS | +---------------+ |
| | REST Client |--+------->| | REST API | |
| +---------------+ | | +---------------+ |
| +---------------+ | LiveKit | +---------------+ |
| | Voice/Video |--+------->| | LiveKit SFU | |
| +---------------+ | | +---------------+ |
+---------------------+ | +---------------+ |
| | SQLite DB | |
| +---------------+ |
+---------------------+
Build and Test
Prerequisites
- Go 1.26+
- Node.js 24+ (see
Client/.nvmrc) - Rust stable (client builds)
Build from source
# Server (Windows)
cd Server
go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.4" .
# Server (Linux)
cd Server
CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.4" .
# Client
cd Client
npm install
npm run tauri build
Core verification commands
Everything CI gates on, from the repository root:
npm run check # server + client + Rust
npm run check:server # or one stack at a time
node scripts/run.mjs --list # exactly what each task runs, and where
Or run the stacks directly — the facade is a convenience, not the only path, and server work needs no Node at all:
# Server
cd Server
go test ./...
# Client
cd Client
npm run typecheck
npm run lint
npm test
For the full command set, use docs/contributing.md.
Configuration
On first run, the server generates config.yaml and a local data/ directory:
data/
├── chatserver.db
├── certs/
├── uploads/
└── backups/
Key options include TLS mode, upload limits, LiveKit settings, and admin CIDR restrictions. See docs/server-configuration.md.
Security and Vulnerability Reporting
- For vulnerabilities, use GitHub Security Advisories (private disclosure flow).
- Do not open public issues for security bugs.
- Read full policy and hardening notes in docs/security.md.
Update Signing Notes (Maintainers)
Client and server update signing keys are intentionally separate.
Required Actions secrets for release signing:
TAURI_SIGNING_PRIVATE_KEYTAURI_SIGNING_PRIVATE_KEY_PASSWORDSERVER_UPDATE_SIGNING_PRIVATE_KEYSERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD
When rotating the server updater key, update Server/updater/server_update_public_key.txt and use staged rollover for live fleets.
Docs Index
docs/README.md is the complete index — every document in
docs/, grouped by whether it is guidance, a reference contract, a dated audit,
or a plan. The most-used entries:
- docs/quick-start.md — get a server running
- docs/deployment.md — production deployment
- docs/contributing.md — setup, branch model, how to run the checks CI runs
- docs/security.md — reporting a vulnerability
- docs/architecture/ — system blueprints (diagrams + flows)
- docs/architecture/ux/ — client UX specification (target-state flows, per-view states, event→reaction maps)
- docs/api.md, docs/protocol.md, docs/schema.md, docs/server-configuration.md — reference contracts
- docs/plans/README.md — plan index; records each plan's state and is the authority over a plan's own header
Audits are dated snapshots and are not maintained after the fact — read them as history. docs/README.md lists all nine, newest first.
Contributing
- Create a branch from
dev(the active development branch). - Keep changes focused and tested.
- Open a PR targeting
dev—devis merged tomainfor releases.
See docs/contributing.md for the full process.
Getting Help and Reporting Problems
Nothing here is a support promise — see the note at the top — but there is a right place for each kind of message:
| Kind | Where |
|---|---|
| A reproducible bug | Issues |
| A question about setup or usage | Discussions → Q&A |
| An idea or feature suggestion | Discussions → Ideas |
| A security vulnerability | Private advisory — never an issue |
License
AGPL-3.0


