* fix(docker): build the server image with Go 1.26 The Docker verify job failed with "go.mod requires go >= 1.26 (running go 1.25.12; GOTOOLCHAIN=local)". The Go 1.26 upgrade bumped go.mod but left the Dockerfile on golang:1.25-bookworm, and GOTOOLCHAIN=local in the base image means it cannot download a newer toolchain. golang:1.26-bookworm confirmed present upstream. Not verified locally (Docker Desktop not running); the CI Docker job proves it on this PR. * fix(client): override brace-expansion and qs to patched versions npm audit --audit-level=high failed the Client Static Checks job with 10 vulnerabilities (8 high, 2 moderate). npm audit fix could not resolve any of them. There is really only one advisory behind the eight high findings: brace-expansion <=5.0.7, a DoS via unbounded expansion length causing OOM. minimatch, glob, test-exclude, @vitest/coverage-v8, eslint and @eslint/* were all just transitive consumers of it, and those top-level dev deps are already at their latest versions, so no bump reaches the fix. qs 6.11.1-6.15.1 is a second, independent advisory arriving via @stryker-mutator/core -> typed-rest-client. No patch exists inside the brace-expansion 1.x or 2.x lines (the fix landed in 5.0.8), so overrides are the only route. Collapsing every copy to 5.0.9 risked breaking minimatch 3.x, which requires it as CJS, so the whole client gate was run to check: npm audit 0 vulnerabilities, tsc clean, oxlint unchanged (pre-existing no-underscore-dangle warnings only), eslint exit 0, prettier clean, and vitest 3572 tests across 129 files all passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: stop running the suite twice for one push to dev Listing dev under both push and pull_request meant a single push to dev fired both events, running every job twice (visible as duplicated checks on #1274). While a dev -> main PR is open, pull_request(synchronize) already covers each push to dev, so dev only needs the pull_request trigger. workflow_dispatch covers a dev branch with no PR open yet. * chore(deps): roll up the seven open dependabot PRs Consolidates #1267-#1273 onto this branch so they land as one CI run instead of seven, each of which was triggering the full suite including tauri-build. - google.golang.org/grpc 1.81.1 -> 1.82.1 (#1267) - github.com/google/cel-go 0.28.1 -> 0.29.0 (#1268) - defu 6.1.4 -> 6.1.7, root lockfile (#1269) - tauri 2.11.0 -> 2.11.1 (#1270) - @modelcontextprotocol/sdk 1.29 -> 1.30 (#1271) - tar 0.4.45 -> 0.4.46 (#1272) - serde_with 3.18.0 -> 3.21.0 (#1273) Applied by regenerating each lockfile from its manifest rather than merging seven lockfile diffs. Verified: go build across all four tag variants, go vet, govulncheck (0 vulnerabilities in called code), go test -race (14 packages, 0 failures), cargo clippy --all-targets -D warnings, cargo test --lib (73 passed). CI covers neither the root package.json nor tools/mcp-introspect, so those two were checked by hand: changelogen still runs under defu 6.1.7 (release.yml depends on it) and the introspect server still imports the 1.30 SDK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): drop the brace-expansion override, scope the audit to shipped deps The brace-expansion override I added to clear npm audit broke Client Unit Tests in CI: TypeError: (0 , brace_expansion_1.default) is not a function at minimatch braceExpand -> TestExclude.glob -> V8CoverageProvider.getUntestedFiles minimatch requires brace-expansion as CJS and v5 is not callable that way. It only fires under --coverage, which is why a local `vitest run` missed it; CI runs `vitest run --coverage`. Verified the fix with that exact command. There is no patched brace-expansion in the 1.x/2.x lines those tools pin (the fix landed in 5.0.8), and eslint, @vitest/coverage-v8 and stryker are already latest, so no bump reaches it. Since the whole chain is dev tooling that never ships, the gate is now `npm audit --omit=dev --audit-level=high`, which reports 0 vulnerabilities. The reasoning and the revisit condition are recorded in ci.yml next to the step. The qs override stays: qs is CJS, the override is proven safe, and it closes a real advisory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(client): route all cert-tofu emits through the single call site Tauri Full Build failed on all three platforms with the generated bindings redeclaring onCertTofu (TS2323/TS2393), which killed `tauri build` at its beforeBuildCommand: src/generated/events.ts(36,23): error TS2323: Cannot redeclare exported variable 'onCertTofu'. tauri-typegen emits one onCertTofu binding per `emit("cert-tofu", ..)` call site it finds. ws_proxy.rs already funnelled its emits through a helper for exactly this reason -- its doc comment says so -- but http_proxy.rs emitted directly from all three TOFU outcomes, so the crate had four call sites. Makes ws_proxy::emit_cert_tofu pub(crate) and routes http_proxy's trusted, first_use and mismatch paths through it, leaving one call site crate-wide. The now-unused Emitter import is dropped from http_proxy so clippy -D warnings stays clean. Behaviour is unchanged: same event name, same payloads, same order. Not reproducible locally -- typegen only regenerates under CI's clean checkout, and a full `npm run tauri build` here passes tsc either way -- so the Tauri Full Build job on this PR is the proof. Verified locally: exactly one emit("cert-tofu") call site remains, cargo clippy --all-targets -D warnings clean, and the release build completes through bundling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <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, there's no support, 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 | First full review pass landed in v1.1.0-alpha.2; further review passes since; ongoing |
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 create your Owner account. - 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
- 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
- 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 20+
- Rust stable (client builds)
Build from source
# Server (Windows)
cd Server
go build -o chatserver.exe -ldflags "-s -w -X main.version=1.1.0-alpha.2" .
# Server (Linux)
cd Server
CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.1.0-alpha.2" .
# Client
cd Client/tauri-client
npm install
npm run tauri build
Core verification commands
# Server
cd Server
go test ./...
# Client
cd Client/tauri-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/quick-start.md
- docs/deployment.md
- docs/livekit-setup.md
- docs/port-forwarding.md
- docs/tailscale.md
- docs/architecture/ — system blueprints (diagrams + flows)
- docs/audit-2026-07-19.md — latest architecture & spec-conformance audit
- docs/api.md
- docs/protocol.md
- docs/schema.md
- docs/architecture/client.md — client architecture (replaces client-architecture.md)
- docs/architecture/ux/ — client UX specification (target-state flows, per-view states, event→reaction maps)
- docs/contributing.md
- docs/security.md
Contributing
- Create a branch from
main. - Keep changes focused and tested.
- Open a PR targeting
main.
See docs/contributing.md for the full process.
License
AGPL-3.0


