mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
docs: add public documentation for contributors and users
Created 12 public docs derived from internal vault: - Setup guides: quick-start, server-configuration, livekit-setup, deployment - Networking: port-forwarding, tailscale - References: api, protocol, schema, client-architecture - Community: contributing, security Updated .gitignore to only exclude docs/brain/ (internal vault), allowing docs/ to be tracked. Updated README with expanded quick start, voice/video setup, networking ports, and doc links.
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ CLAUDE.md
|
||||
.github/instructions/
|
||||
|
||||
# AI-specific / internal planning docs
|
||||
docs/
|
||||
docs/brain/
|
||||
skills/
|
||||
|
||||
# Server runtime artifacts
|
||||
|
||||
@@ -107,13 +107,46 @@ dependencies, works fully on LAN.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Download the latest release from
|
||||
1. Download `chatserver.exe` and the OwnCord installer from
|
||||
[GitHub Releases](https://github.com/J3vb/OwnCord/releases)
|
||||
2. Run `chatserver.exe` — generates `config.yaml` on first run
|
||||
3. Open `https://localhost:8443/admin` to access the admin panel
|
||||
4. Generate an invite code and share it with friends
|
||||
5. Friends download the client installer and connect
|
||||
using your server address
|
||||
2. Run `chatserver.exe` — generates `config.yaml` and a `data/`
|
||||
directory (database, TLS certs, uploads, backups) on first run
|
||||
3. Open `https://localhost:8443/admin` to create the Owner account
|
||||
4. Generate an invite code in the admin panel and share it
|
||||
5. Friends install the client, enter your server address
|
||||
(`ip:8443`), and register with the invite code
|
||||
|
||||
The client uses TOFU (Trust On First Use) for self-signed
|
||||
certificates — it prompts to trust the server on first
|
||||
connection, then pins it for future sessions.
|
||||
|
||||
### Voice & Video Setup (Optional)
|
||||
|
||||
Voice and video require [LiveKit Server](https://github.com/livekit/livekit/releases):
|
||||
|
||||
1. Download `livekit-server` from the LiveKit releases page
|
||||
2. Edit `config.yaml` and set:
|
||||
```yaml
|
||||
voice:
|
||||
livekit_api_key: "devkey" # any string
|
||||
livekit_api_secret: "secret-min-32-characters-long!!" # min 32 chars
|
||||
livekit_binary: "C:/path/to/livekit-server.exe"
|
||||
```
|
||||
3. Restart `chatserver.exe` — it auto-starts LiveKit as a
|
||||
companion process
|
||||
|
||||
### Networking
|
||||
|
||||
For friends outside your LAN, you need to forward these ports:
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
| ---- | -------- | ------- |
|
||||
| `8443` | TCP | HTTPS, WebSocket, REST API |
|
||||
| `7881` | TCP | LiveKit signaling (voice/video) |
|
||||
| `50000-60000` | UDP | LiveKit WebRTC media (voice/video) |
|
||||
|
||||
Alternatively, use Tailscale for zero-config networking
|
||||
with no port forwarding.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -223,7 +256,18 @@ npm run lint:fix # ESLint auto-fix
|
||||
|
||||
## Configuration
|
||||
|
||||
The server generates a `config.yaml` on first run. Key settings:
|
||||
The server generates a `config.yaml` on first run. All runtime data
|
||||
is stored in a `data/` directory alongside the executable:
|
||||
|
||||
```text
|
||||
data/
|
||||
├── owncord.db # SQLite database
|
||||
├── certs/ # TLS certificates (auto-generated if self_signed)
|
||||
├── uploads/ # User-uploaded files
|
||||
└── backups/ # Database backups
|
||||
```
|
||||
|
||||
Key settings:
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ------- | ------- | ----------- |
|
||||
@@ -252,19 +296,27 @@ To enable signed releases in CI, add these GitHub repository secrets:
|
||||
|
||||
## Documentation
|
||||
|
||||
Detailed docs live in the `docs/brain/` Obsidian vault:
|
||||
- [Quick Start Guide](docs/quick-start.md)
|
||||
- [Server Configuration](docs/server-configuration.md)
|
||||
- [LiveKit Setup (Voice/Video)](docs/livekit-setup.md)
|
||||
- [Deployment Guide](docs/deployment.md)
|
||||
- [Port Forwarding](docs/port-forwarding.md)
|
||||
- [Tailscale Guide](docs/tailscale.md)
|
||||
- [REST API Reference](docs/api.md)
|
||||
- [WebSocket Protocol](docs/protocol.md)
|
||||
- [Database Schema](docs/schema.md)
|
||||
- [Client Architecture](docs/client-architecture.md)
|
||||
- [Contributing](docs/contributing.md)
|
||||
- [Security Policy](docs/security.md)
|
||||
|
||||
- [Quick Start Guide](docs/brain/08-Guides/quick-start.md)
|
||||
- [Port Forwarding Guide](docs/brain/08-Guides/port-forwarding.md)
|
||||
- [Tailscale Guide](docs/brain/08-Guides/tailscale.md)
|
||||
- [Client Architecture](docs/brain/06-Specs/CLIENT-ARCHITECTURE.md)
|
||||
- [Server Spec](docs/brain/06-Specs/CHATSERVER.md)
|
||||
- [WebSocket Protocol](docs/brain/06-Specs/PROTOCOL.md)
|
||||
- [REST API](docs/brain/06-Specs/API.md)
|
||||
- [Database Schema](docs/brain/06-Specs/SCHEMA.md)
|
||||
- [Testing Strategy](docs/brain/06-Specs/TESTING-STRATEGY.md)
|
||||
- [Contributing](docs/brain/08-Guides/CONTRIBUTING.md)
|
||||
- [Security](docs/brain/08-Guides/SECURITY.md)
|
||||
## Contributing
|
||||
|
||||
1. Fork the repo and create a feature branch from `dev`
|
||||
2. Follow existing code style and conventions
|
||||
3. Write tests for new functionality
|
||||
4. Open a PR against `dev` with a clear description
|
||||
|
||||
See [Contributing Guide](docs/contributing.md) for details.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
|
||||
+849
@@ -0,0 +1,849 @@
|
||||
# REST API Reference
|
||||
|
||||
OwnCord server REST API reference. All endpoints use the base URL `https://{server}:{port}/api/v1`.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All authenticated endpoints require a session token delivered via the `Authorization: Bearer {token}` header. Tokens are obtained from `POST /api/v1/auth/login`, `POST /api/v1/auth/register`, or `POST /api/v1/auth/verify-totp` after a partial 2FA challenge.
|
||||
|
||||
### Session Lifecycle
|
||||
|
||||
- Sessions are created on login/register and stored with a SHA-256 hash of the raw token, the client IP, User-Agent, and an expiry timestamp.
|
||||
- Each authenticated request updates the session's `last_active` timestamp.
|
||||
- Banned users are rejected at the middleware level with `403 FORBIDDEN`.
|
||||
|
||||
### Middleware Stack (all routes)
|
||||
|
||||
1. **RequestID** -- assigns a unique `X-Request-Id` response header.
|
||||
2. **Recoverer** -- catches panics and returns 500.
|
||||
3. **Request Logger** -- structured logging of method, path, status, duration.
|
||||
4. **SecurityHeaders** -- sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `X-XSS-Protection: 0`, `Referrer-Policy: strict-origin-when-cross-origin`, `Content-Security-Policy: default-src 'self'`, `Permissions-Policy: camera=(), microphone=(), geolocation=()`, `Cache-Control: no-store`.
|
||||
5. **MaxBodySize** -- 1 MiB default for all routes except `/api/v1/uploads` (which has its own 100 MiB limit).
|
||||
|
||||
---
|
||||
|
||||
## Standard Error Response
|
||||
|
||||
All error responses use this JSON envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "ERROR_CODE",
|
||||
"message": "Human-readable detail"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | HTTP Status | When It Occurs |
|
||||
| ---- | ----------- | -------------- |
|
||||
| `UNAUTHORIZED` | 401 | Missing/invalid/expired session token |
|
||||
| `INVALID_CREDENTIALS` | 401 | Login/register with bad username/password/invite (generic to prevent enumeration) |
|
||||
| `FORBIDDEN` | 403 | Insufficient permissions, banned account, or admin IP restriction |
|
||||
| `NOT_FOUND` | 404 | Resource (channel, message, user, invite, file, backup) not found |
|
||||
| `RATE_LIMITED` | 429 | Too many requests; response includes `Retry-After` header (seconds) |
|
||||
| `INVALID_INPUT` / `BAD_REQUEST` | 400 | Malformed body, missing required fields, invalid query params |
|
||||
| `CONFLICT` | 409 | Duplicate username on register, or server already up-to-date on update |
|
||||
| `TOO_LARGE` | 413 | File exceeds upload size limit |
|
||||
| `SERVER_ERROR` / `INTERNAL` | 500 | Internal server error |
|
||||
| `BAD_GATEWAY` | 502 | Upstream failure (GitHub API, LiveKit, asset download) |
|
||||
|
||||
---
|
||||
|
||||
## Auth Endpoints
|
||||
|
||||
### POST /api/v1/auth/register
|
||||
|
||||
Create a new account using an invite code. The first user is created via `/admin/api/setup` instead.
|
||||
|
||||
**Auth:** None (public)
|
||||
**Rate limit:** 3 requests/minute per IP
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "alex",
|
||||
"password": "MyStr0ng!Pass",
|
||||
"invite_code": "abc123def"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
| ----- | ---- | -------- | ----- |
|
||||
| `username` | string | Yes | HTML-stripped, trimmed. Must be non-empty. |
|
||||
| `password` | string | Yes | Validated for strength (min length, complexity). |
|
||||
| `invite_code` | string | Yes | Must be a valid, non-expired, non-revoked invite with remaining uses. |
|
||||
|
||||
#### Response 201 Created
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "raw-session-token-64-chars",
|
||||
"user": {
|
||||
"id": 2,
|
||||
"username": "alex",
|
||||
"avatar": "",
|
||||
"status": "offline",
|
||||
"role_id": 4,
|
||||
"totp_enabled": false,
|
||||
"created_at": "2026-03-24T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Status | Code | Cause |
|
||||
| ------ | ---- | ----- |
|
||||
| 400 | `INVALID_INPUT` | Missing username/password/invite_code, or weak password |
|
||||
| 400 | `INVALID_CREDENTIALS` | Bad invite code, expired/revoked invite, or duplicate username |
|
||||
| 403 | `FORBIDDEN` | Registration is closed or unavailable while server-wide 2FA is required |
|
||||
| 429 | `RATE_LIMITED` | Exceeded 3 registrations/minute from this IP |
|
||||
| 500 | `SERVER_ERROR` | Hashing failure, session creation failure, or DB error |
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/auth/login
|
||||
|
||||
Authenticate with username and password.
|
||||
|
||||
**Auth:** None (public)
|
||||
**Rate limit:** 60 requests/minute per IP. After 10 consecutive failures from the same IP, the IP is locked out for 15 minutes.
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "alex",
|
||||
"password": "MyStr0ng!Pass"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
If the account does not have TOTP enabled:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "raw-session-token-64-chars",
|
||||
"requires_2fa": false,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "alex",
|
||||
"avatar": "uuid.png",
|
||||
"status": "offline",
|
||||
"role_id": 4,
|
||||
"totp_enabled": false,
|
||||
"created_at": "2026-03-24T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the account has TOTP enabled:
|
||||
|
||||
```json
|
||||
{
|
||||
"partial_token": "opaque-partial-token",
|
||||
"requires_2fa": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Status | Code | Cause |
|
||||
| ------ | ---- | ----- |
|
||||
| 400 | `INVALID_INPUT` | Missing username or password |
|
||||
| 401 | `UNAUTHORIZED` | Wrong username or password |
|
||||
| 403 | `FORBIDDEN` | Account is banned/suspended |
|
||||
| 429 | `RATE_LIMITED` | IP locked out after 10 consecutive failures (15 min cooldown) |
|
||||
| 500 | `SERVER_ERROR` | Session creation failure |
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/auth/verify-totp
|
||||
|
||||
Complete a TOTP login challenge started by `POST /api/v1/auth/login`.
|
||||
|
||||
**Auth:** Required with the `partial_token` from the login response
|
||||
**Rate limit:** 10 requests/minute per IP, plus a 5-attempt budget per partial challenge
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "raw-session-token-64-chars",
|
||||
"requires_2fa": false,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "alex",
|
||||
"avatar": "uuid.png",
|
||||
"status": "offline",
|
||||
"role_id": 4,
|
||||
"totp_enabled": true,
|
||||
"created_at": "2026-03-24T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Status | Code | Cause |
|
||||
| ------ | ---- | ----- |
|
||||
| 400 | `INVALID_INPUT` | Malformed request body |
|
||||
| 401 | `UNAUTHORIZED` | Missing/expired challenge, invalid TOTP code, or challenge consumed |
|
||||
| 500 | `SERVER_ERROR` | Session creation failure |
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/auth/me
|
||||
|
||||
Get the current authenticated user's profile.
|
||||
|
||||
**Auth:** Required (Bearer token)
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "alex",
|
||||
"avatar": "uuid.png",
|
||||
"status": "online",
|
||||
"role_id": 2,
|
||||
"totp_enabled": true,
|
||||
"created_at": "2026-03-24T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `id` | int64 | User ID |
|
||||
| `username` | string | Display name |
|
||||
| `avatar` | string | Avatar filename (UUID) or empty string |
|
||||
| `status` | string | One of: `online`, `idle`, `dnd`, `offline` |
|
||||
| `role_id` | int64 | Numeric role ID (1=Owner, 2=Admin, 3=Moderator, 4=Member) |
|
||||
| `totp_enabled` | bool | Whether the user has a confirmed TOTP secret |
|
||||
| `created_at` | string | ISO 8601 timestamp |
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/auth/logout
|
||||
|
||||
Invalidate the current session token.
|
||||
|
||||
**Auth:** Required (Bearer token)
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/users/me/totp/enable
|
||||
|
||||
Start TOTP enrollment for the authenticated user. The secret is not persisted until `/api/v1/users/me/totp/confirm` succeeds.
|
||||
|
||||
**Auth:** Required
|
||||
**Rate limit:** 5 requests/minute per IP
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"password": "MyStr0ng!Pass"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"qr_uri": "otpauth://totp/OwnCord:alex?...",
|
||||
"backup_codes": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/users/me/totp/confirm
|
||||
|
||||
Confirm a pending TOTP enrollment.
|
||||
|
||||
**Auth:** Required
|
||||
**Rate limit:** 5 requests/minute per IP
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"password": "MyStr0ng!Pass",
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/v1/users/me/totp
|
||||
|
||||
Disable TOTP for the authenticated user.
|
||||
|
||||
**Auth:** Required
|
||||
**Rate limit:** 5 requests/minute per IP
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"password": "MyStr0ng!Pass"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
## Channel Endpoints
|
||||
|
||||
### GET /api/v1/channels
|
||||
|
||||
List all channels the authenticated user has `READ_MESSAGES` permission for. DM channels are NOT included (use `GET /api/v1/dms` instead).
|
||||
|
||||
**Auth:** Required
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "general",
|
||||
"type": "text",
|
||||
"topic": "Welcome to the server!",
|
||||
"category": "Text Channels",
|
||||
"position": 0,
|
||||
"slow_mode": 0,
|
||||
"archived": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `id` | int64 | Channel ID |
|
||||
| `name` | string | Channel name |
|
||||
| `type` | string | `text`, `voice`, or `announcement` |
|
||||
| `topic` | string | Channel topic/description |
|
||||
| `category` | string | Category grouping |
|
||||
| `position` | int | Sort order within category |
|
||||
| `slow_mode` | int | Slow-mode delay in seconds (0 = disabled) |
|
||||
| `archived` | bool | Whether the channel is archived |
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/channels/{id}/messages
|
||||
|
||||
Paginated message history for a channel.
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `READ_MESSAGES` on the channel (or DM participant membership)
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Param | Type | Default | Range | Description |
|
||||
| ----- | ---- | ------- | ----- | ----------- |
|
||||
| `before` | int64 | 0 (latest) | >= 0 | Cursor: return messages with ID less than this value |
|
||||
| `limit` | int | 50 | 1-100 | Number of messages to return |
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": 1042,
|
||||
"channel_id": 5,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "alex",
|
||||
"avatar": "uuid.png"
|
||||
},
|
||||
"content": "Hello!",
|
||||
"reply_to": null,
|
||||
"attachments": [
|
||||
{
|
||||
"id": "file-uuid",
|
||||
"filename": "photo.jpg",
|
||||
"size": 204800,
|
||||
"mime_type": "image/jpeg",
|
||||
"url": "/api/v1/files/file-uuid",
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
}
|
||||
],
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": "\ud83d\udc4d",
|
||||
"count": 2,
|
||||
"me": true
|
||||
}
|
||||
],
|
||||
"pinned": false,
|
||||
"edited_at": null,
|
||||
"deleted": false,
|
||||
"timestamp": "2026-03-14T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Pagination
|
||||
|
||||
Use cursor-based pagination by passing the `id` of the last message as the `before` parameter:
|
||||
|
||||
```
|
||||
GET /api/v1/channels/5/messages?before=1042&limit=50
|
||||
```
|
||||
|
||||
When `has_more` is `false`, you have reached the beginning of the channel history.
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/channels/{id}/pins
|
||||
|
||||
Get all pinned messages for a channel.
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `READ_MESSAGES` on the channel
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
Returns `{ "messages": [...], "has_more": false }`. `has_more` is always `false` for pins (all pinned messages are returned at once).
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/channels/{id}/pins/{messageId}
|
||||
|
||||
Pin a message in a channel.
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `MANAGE_MESSAGES` on the channel
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/v1/channels/{id}/pins/{messageId}
|
||||
|
||||
Unpin a message from a channel.
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `MANAGE_MESSAGES` on the channel
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
## Search
|
||||
|
||||
### GET /api/v1/search
|
||||
|
||||
Full-text search across messages in channels the user can read. Uses SQLite FTS5 for matching.
|
||||
|
||||
**Auth:** Required
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Param | Type | Default | Range | Description |
|
||||
| ----- | ---- | ------- | ----- | ----------- |
|
||||
| `q` | string | (required) | non-empty | Search query (FTS5 syntax) |
|
||||
| `channel_id` | int64 | (all channels) | > 0 | Restrict search to a single channel |
|
||||
| `limit` | int | 50 | 1-100 | Maximum results to return |
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"message_id": 1042,
|
||||
"channel_id": 5,
|
||||
"channel_name": "general",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "alex"
|
||||
},
|
||||
"content": "...matched text...",
|
||||
"timestamp": "2026-03-14T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Direct Messages
|
||||
|
||||
DM channels use participant-based authorization rather than role-based permissions.
|
||||
|
||||
### POST /api/v1/dms
|
||||
|
||||
Create or retrieve a 1-on-1 DM channel with another user. If a DM channel already exists, it is returned and re-opened.
|
||||
|
||||
**Auth:** Required
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"recipient_id": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 200 OK (existing channel) or 201 Created (new channel)
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": 100,
|
||||
"recipient": {
|
||||
"id": 2,
|
||||
"username": "jordan",
|
||||
"avatar": "uuid.png",
|
||||
"status": "online"
|
||||
},
|
||||
"created": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/dms
|
||||
|
||||
List all open DM channels for the authenticated user, ordered by most recent activity.
|
||||
|
||||
**Auth:** Required
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"dm_channels": [
|
||||
{
|
||||
"channel_id": 100,
|
||||
"recipient": {
|
||||
"id": 2,
|
||||
"username": "jordan",
|
||||
"avatar": "uuid.png",
|
||||
"status": "online"
|
||||
},
|
||||
"last_message_id": 5042,
|
||||
"last_message": "Hey, how's it going?",
|
||||
"last_message_at": "2026-03-28T14:30:00Z",
|
||||
"unread_count": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/v1/dms/{channelId}
|
||||
|
||||
Close a DM channel for the authenticated user (hides it from their sidebar). The channel and messages remain in the database. If the other user sends a new message, the channel is automatically re-opened.
|
||||
|
||||
**Auth:** Required
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
## Invite Endpoints
|
||||
|
||||
All invite endpoints require authentication and the `MANAGE_INVITES` permission.
|
||||
|
||||
### POST /api/v1/invites
|
||||
|
||||
Create a new invite code.
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `MANAGE_INVITES`
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"max_uses": 5,
|
||||
"expires_in_hours": 48
|
||||
}
|
||||
```
|
||||
|
||||
Both fields are optional. An empty body creates an invite with unlimited uses and no expiry.
|
||||
|
||||
#### Response 201 Created
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"code": "abc123def",
|
||||
"max_uses": 5,
|
||||
"uses": 0,
|
||||
"expires_at": "2026-03-30T10:30:00Z",
|
||||
"revoked": false,
|
||||
"created_at": "2026-03-28T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/invites
|
||||
|
||||
List all invites (active, expired, and revoked).
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `MANAGE_INVITES`
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
Returns a JSON array of invite objects.
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/v1/invites/{code}
|
||||
|
||||
Revoke an invite by its code string.
|
||||
|
||||
**Auth:** Required
|
||||
**Permission:** `MANAGE_INVITES`
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
---
|
||||
|
||||
## File Upload and Serving
|
||||
|
||||
### POST /api/v1/uploads
|
||||
|
||||
Upload a file as multipart form data.
|
||||
|
||||
**Auth:** Required
|
||||
**Body size limit:** 100 MiB
|
||||
**Content-Type:** `multipart/form-data`
|
||||
|
||||
Files are validated against blocked magic bytes (PE executables, ELF binaries, Mach-O binaries, shell scripts). Files are stored with UUID filenames.
|
||||
|
||||
#### Response 201 Created
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"filename": "photo.jpg",
|
||||
"size": 204800,
|
||||
"mime": "image/jpeg",
|
||||
"url": "/api/v1/files/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
}
|
||||
```
|
||||
|
||||
`width` and `height` are only present for image files.
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/files/{id}
|
||||
|
||||
Serve a previously uploaded file by its UUID.
|
||||
|
||||
**Auth:** None (URLs are unguessable UUIDs)
|
||||
**Caching:** `Cache-Control: public, max-age=31536000, immutable`
|
||||
|
||||
Supports HTTP range requests and conditional requests.
|
||||
|
||||
---
|
||||
|
||||
## Health Check
|
||||
|
||||
### GET /health
|
||||
|
||||
### GET /api/v1/health
|
||||
|
||||
Public health check endpoint, no authentication required.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"uptime": 86400,
|
||||
"online_users": 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Info
|
||||
|
||||
### GET /api/v1/info
|
||||
|
||||
Returns the server name and version.
|
||||
|
||||
**Auth:** None
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My OwnCord Server",
|
||||
"version": "1.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metrics
|
||||
|
||||
### GET /api/v1/metrics
|
||||
|
||||
Runtime server metrics. Restricted to admin-allowed CIDRs.
|
||||
|
||||
**Auth:** Admin IP restriction (not token-based)
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime": "2h30m15s",
|
||||
"uptime_seconds": 9015.0,
|
||||
"goroutines": 42,
|
||||
"heap_alloc_mb": 12.5,
|
||||
"heap_sys_mb": 24.0,
|
||||
"num_gc": 156,
|
||||
"connected_users": 8,
|
||||
"livekit_healthy": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LiveKit Endpoints
|
||||
|
||||
These endpoints are only registered when LiveKit voice is configured.
|
||||
|
||||
### POST /api/v1/livekit/webhook
|
||||
|
||||
LiveKit webhook receiver. Uses LiveKit JWT verification. Admin-IP-restricted. Called by the LiveKit server, not by clients.
|
||||
|
||||
### GET /api/v1/livekit/health
|
||||
|
||||
Check whether the LiveKit server is reachable.
|
||||
|
||||
**Auth:** Admin IP restriction
|
||||
|
||||
#### Response 200 OK
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"livekit_reachable": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 503 Service Unavailable
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "degraded",
|
||||
"livekit_reachable": false,
|
||||
"error": "connection refused"
|
||||
}
|
||||
```
|
||||
|
||||
### /livekit/* (Reverse Proxy)
|
||||
|
||||
All requests to `/livekit/*` are reverse-proxied to the LiveKit server URL. The `/livekit` prefix is stripped before forwarding. This allows the client to connect to LiveKit through OwnCord's HTTPS server, avoiding mixed-content blocks.
|
||||
|
||||
**Auth:** None (LiveKit handles its own JWT-based auth)
|
||||
**Rate limit:** 30 requests/minute per IP
|
||||
|
||||
---
|
||||
|
||||
## Diagnostics
|
||||
|
||||
### GET /api/v1/diagnostics/connectivity
|
||||
|
||||
Returns connectivity diagnostics for debugging voice/network issues.
|
||||
|
||||
**Auth:** Required (any authenticated user)
|
||||
**Rate limit:** 5 requests/minute per user
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"version": "1.0.0",
|
||||
"uptime_s": 3600,
|
||||
"go_version": "go1.23.0",
|
||||
"online_users": 5
|
||||
},
|
||||
"voice": {
|
||||
"enabled": true,
|
||||
"livekit_url": "ws://localhost:7880",
|
||||
"livekit_health": true,
|
||||
"node_ip": "203.0.113.1",
|
||||
"proxy_path": "/livekit"
|
||||
},
|
||||
"client": {
|
||||
"remote_addr": "192.168.1.100",
|
||||
"is_private_network": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client Auto-Update
|
||||
|
||||
### GET /api/v1/client-update/{target}/{current_version}
|
||||
|
||||
Tauri-compatible update endpoint. The desktop client checks this to see if a newer version is available.
|
||||
|
||||
**Auth:** None
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
| Param | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `target` | string | Platform target (e.g., `windows-x86_64`) |
|
||||
| `current_version` | string | Client's current semver version (e.g., `1.0.0`) |
|
||||
|
||||
#### Response 200 OK (update available)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.2.0",
|
||||
"notes": "## What's Changed\n...",
|
||||
"pub_date": "2026-03-28T00:00:00Z",
|
||||
"platforms": {
|
||||
"windows-x86_64": {
|
||||
"signature": "base64-encoded-signature",
|
||||
"url": "https://github.com/J3vb/OwnCord/releases/download/v1.2.0/OwnCord_1.2.0_x64-setup.nsis.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Response 204 No Content
|
||||
|
||||
Client is already up-to-date.
|
||||
|
||||
---
|
||||
|
||||
## WebSocket
|
||||
|
||||
### GET /api/v1/ws
|
||||
|
||||
WebSocket upgrade endpoint. Authentication is performed in-band (first message must be an `auth` frame with the session token). See [protocol.md](protocol.md) for the full WebSocket message protocol.
|
||||
@@ -0,0 +1,280 @@
|
||||
# Client Architecture: Tauri v2
|
||||
|
||||
Comprehensive architecture reference for the OwnCord Tauri v2 desktop client. Covers project structure, data flow, component system, and subsystems.
|
||||
|
||||
## Why Tauri v2
|
||||
|
||||
Tauri v2 uses the OS webview (WebView2 on Windows) so the install is ~10-15 MB and RAM usage is ~30-50 MB. The HTML/CSS mockups become the actual UI code, with CSS handling hover effects, conditional visibility, theming, and animations.
|
||||
|
||||
---
|
||||
|
||||
## Project Layout
|
||||
|
||||
```text
|
||||
Client/tauri-client/
|
||||
├── src-tauri/ # Rust backend
|
||||
│ ├── Cargo.toml
|
||||
│ ├── tauri.conf.json # Window size, title, plugins, CSP, updater
|
||||
│ └── src/
|
||||
│ ├── main.rs # Windows entry point
|
||||
│ ├── lib.rs # Tauri Builder: plugins, commands, state
|
||||
│ ├── credentials.rs # Win Credential Manager (DPAPI)
|
||||
│ ├── commands.rs # Settings store, cert fingerprints, DevTools
|
||||
│ ├── ws_proxy.rs # WSS proxy with TOFU cert pinning
|
||||
│ ├── livekit_proxy.rs # TCP-to-TLS tunnel for LiveKit signaling
|
||||
│ ├── ptt.rs # Push-to-talk via GetAsyncKeyState
|
||||
│ ├── tray.rs # System tray icon and menu
|
||||
│ ├── hotkeys.rs # Global shortcut registration
|
||||
│ └── update_commands.rs # Auto-update check + install
|
||||
│
|
||||
├── src/ # TypeScript frontend
|
||||
│ ├── index.html # Single HTML entry point
|
||||
│ ├── main.ts # Bootstrap, router, service wiring
|
||||
│ │
|
||||
│ ├── styles/
|
||||
│ │ ├── tokens.css # CSS custom properties
|
||||
│ │ ├── base.css # Reset, scrollbar, typography
|
||||
│ │ ├── login.css # ConnectPage styles
|
||||
│ │ ├── app.css # MainPage + component styles
|
||||
│ │ └── theme-neon-glow.css # Default theme overrides
|
||||
│ │
|
||||
│ ├── lib/ # Core services (no UI, no DOM)
|
||||
│ │ ├── api.ts # REST client (Tauri plugin-http)
|
||||
│ │ ├── ws.ts # WebSocket client (Tauri IPC proxy)
|
||||
│ │ ├── types.ts # Protocol types (WS + REST + permissions)
|
||||
│ │ ├── store.ts # Reactive store factory
|
||||
│ │ ├── dispatcher.ts # WS message -> store action router
|
||||
│ │ ├── router.ts # In-memory page router
|
||||
│ │ ├── livekitSession.ts # LiveKit voice/video session
|
||||
│ │ ├── connectionStats.ts # WebRTC stats poller
|
||||
│ │ ├── rate-limiter.ts # Sliding-window rate limiter
|
||||
│ │ ├── permissions.ts # Bitfield utilities
|
||||
│ │ ├── profiles.ts # Server profile CRUD
|
||||
│ │ ├── credentials.ts # Credential storage (Tauri IPC)
|
||||
│ │ ├── disposable.ts # Component lifecycle cleanup
|
||||
│ │ ├── dom.ts # XSS-safe DOM helpers
|
||||
│ │ ├── safe-render.ts # Error boundary
|
||||
│ │ ├── logger.ts # Structured logger
|
||||
│ │ ├── notifications.ts # Desktop notifications
|
||||
│ │ ├── tenor.ts # Tenor GIF API v2
|
||||
│ │ ├── themes.ts # Theme manager
|
||||
│ │ ├── updater.ts # Auto-update
|
||||
│ │ ├── reconcile.ts # Keyed DOM list reconciliation
|
||||
│ │ ├── icons.ts # Lucide SVG icon factory
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── stores/ # Reactive state stores
|
||||
│ │ ├── auth.store.ts
|
||||
│ │ ├── channels.store.ts
|
||||
│ │ ├── dm.store.ts
|
||||
│ │ ├── messages.store.ts
|
||||
│ │ ├── members.store.ts
|
||||
│ │ ├── voice.store.ts
|
||||
│ │ └── ui.store.ts
|
||||
│ │
|
||||
│ ├── components/ # UI components
|
||||
│ │ ├── MessageList.ts, MessageInput.ts, ...
|
||||
│ │ ├── message-list/ # MessageList sub-modules
|
||||
│ │ └── settings/ # Settings tab components
|
||||
│ │
|
||||
│ └── pages/
|
||||
│ ├── ConnectPage.ts # Login/register page
|
||||
│ ├── MainPage.ts # Main app layout
|
||||
│ └── main-page/ # MainPage sub-controllers
|
||||
│ ├── SidebarArea.ts
|
||||
│ ├── ChatArea.ts
|
||||
│ ├── ChannelController.ts
|
||||
│ ├── MessageController.ts
|
||||
│ └── ...
|
||||
│
|
||||
├── tests/
|
||||
│ ├── unit/ # Vitest unit tests
|
||||
│ ├── integration/ # Vitest with mocked WS
|
||||
│ └── e2e/ # Playwright E2E tests
|
||||
│
|
||||
├── vite.config.ts
|
||||
├── tsconfig.json
|
||||
├── vitest.config.ts
|
||||
└── playwright.config.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
```text
|
||||
+===================================================================+
|
||||
| UI Components |
|
||||
| (HTML + CSS, vanilla TypeScript DOM manipulation) |
|
||||
| Components are factory functions returning { mount, destroy } |
|
||||
+===================================================================+
|
||||
| | |
|
||||
| subscribe() | actions | events
|
||||
v v v
|
||||
+===================================================================+
|
||||
| Reactive Stores |
|
||||
| auth | channels | dm | messages | members | voice | ui |
|
||||
| Immutable state. Batched notifications via queueMicrotask. |
|
||||
+===================================================================+
|
||||
^ |
|
||||
| WS events | send()
|
||||
+===================================================================+
|
||||
| Core Services |
|
||||
| ws.ts api.ts dispatcher.ts rate-limiter.ts |
|
||||
| livekitSession.ts notifications.ts ptt.ts tenor.ts |
|
||||
+===================================================================+
|
||||
| |
|
||||
| invoke() | listen()
|
||||
v v
|
||||
+===================================================================+
|
||||
| Tauri IPC Bridge |
|
||||
+===================================================================+
|
||||
| ^
|
||||
v |
|
||||
+===================================================================+
|
||||
| Rust Backend |
|
||||
| ws_proxy (WSS + TOFU) livekit_proxy (TCP-to-TLS tunnel) |
|
||||
| credentials (Win32 DPAPI) ptt (GetAsyncKeyState polling) |
|
||||
| commands (settings store) tray hotkeys update_commands |
|
||||
+===================================================================+
|
||||
```
|
||||
|
||||
Data flows DOWN through layers. Events flow UP via subscriptions. No component directly calls the WebSocket or REST API; they go through stores and controllers.
|
||||
|
||||
---
|
||||
|
||||
## Rust Backend Modules
|
||||
|
||||
### ws_proxy.rs -- WebSocket Proxy with TOFU
|
||||
|
||||
WebView2 rejects self-signed TLS certificates. All WebSocket traffic routes through Rust. The Rust proxy implements TOFU certificate pinning -- on first connect, the cert fingerprint is stored; on subsequent connects, it is verified.
|
||||
|
||||
### livekit_proxy.rs -- LiveKit TLS Tunnel
|
||||
|
||||
A local TCP listener proxies LiveKit SDK connections through TLS to the remote server, avoiding self-signed cert issues.
|
||||
|
||||
### credentials.rs -- Windows Credential Manager
|
||||
|
||||
Uses Win32 Credential Manager APIs. Credentials are stored as DPAPI-encrypted blobs tied to the Windows user account.
|
||||
|
||||
### ptt.rs -- Push-to-Talk
|
||||
|
||||
Uses `GetAsyncKeyState` for non-consuming key detection. 20ms polling loop on a background thread.
|
||||
|
||||
### tray.rs -- System Tray
|
||||
|
||||
System tray icon with Show/Hide, Status submenu, and Quit.
|
||||
|
||||
### update_commands.rs -- Auto-Update
|
||||
|
||||
Dynamic server URL updater endpoint. Update artifacts are verified via Ed25519 signature.
|
||||
|
||||
---
|
||||
|
||||
## Store System
|
||||
|
||||
The store factory (`createStore`) provides `getState`, `setState`, `subscribe`, `subscribeSelector`, `select`, and `flush`. State is always immutable. Notifications are batched via `queueMicrotask`.
|
||||
|
||||
### Store Responsibilities
|
||||
|
||||
| Store | Key State | WS Events Handled |
|
||||
|-------|-----------|-------------------|
|
||||
| **auth** | token, user, serverName, motd, isAuthenticated | `auth_ok`, `auth_error` |
|
||||
| **channels** | channels (Map), activeChannelId | `ready`, `channel_create/update/delete` |
|
||||
| **dm** | DM channel list | `dm_channel_open`, `dm_channel_close` |
|
||||
| **messages** | per-channel messages, pending sends, hasMore | `chat_message`, `chat_edited`, `chat_deleted`, `chat_send_ok`, `reaction_update` |
|
||||
| **members** | member Map, typing indicators | `ready`, `member_join/leave/update/ban`, `typing`, `presence` |
|
||||
| **voice** | currentChannelId, voice users, local audio state | `voice_state`, `voice_leave`, `voice_config`, `voice_token` |
|
||||
| **ui** | sidebar mode, modals, theme, connection status | `server_restart`, `error` |
|
||||
|
||||
Messages per channel are capped at 500. Typing indicators auto-clear after 5 seconds.
|
||||
|
||||
---
|
||||
|
||||
## Component System
|
||||
|
||||
Components are factory functions returning `{ mount, destroy }`. `mount()` appends elements to a container; `destroy()` removes DOM, unsubscribes listeners, and clears intervals.
|
||||
|
||||
### DOM List Reconciliation
|
||||
|
||||
For efficient list updates (member list, channel list), a keyed reconciliation algorithm reuses existing DOM elements, updates in place, and removes stale elements -- preserving hover states, focus, and scroll position.
|
||||
|
||||
---
|
||||
|
||||
## Sidebar Architecture
|
||||
|
||||
```text
|
||||
+----------------------------------+
|
||||
| SERVER HEADER |
|
||||
+----------------------------------+
|
||||
| DIRECT MESSAGES (3) [+] |
|
||||
| Top 3 DMs with unread badges |
|
||||
| View all messages link |
|
||||
+----------------------------------+
|
||||
| TEXT CHANNELS |
|
||||
| Category-grouped, collapsible |
|
||||
+----------------------------------+
|
||||
| VOICE CHANNELS |
|
||||
| User avatars in channel |
|
||||
+----------------------------------+
|
||||
| MEMBERS (collapsible) |
|
||||
| Role-grouped, drag-to-resize |
|
||||
+----------------------------------+
|
||||
| VOICE WIDGET |
|
||||
| Mute/deafen/camera/screen/leave |
|
||||
+----------------------------------+
|
||||
| USER BAR |
|
||||
| Settings + quick-switch buttons |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
Two sidebar modes: **"channels"** (full server view) and **"dms"** (full DM conversations list).
|
||||
|
||||
---
|
||||
|
||||
## Chat Area Architecture
|
||||
|
||||
The chat area composes: chat header, message list, typing indicator, message input, video grid (overlays when cameras are active), pinned messages panel, and search overlay.
|
||||
|
||||
The `ChannelController` manages mounting/destroying per-channel components when the active channel changes.
|
||||
|
||||
---
|
||||
|
||||
## Voice and Video (Client Side)
|
||||
|
||||
### LiveKit Session
|
||||
|
||||
The `LiveKitSession` class manages the full voice/video lifecycle via LiveKit's `livekit-client` JS SDK.
|
||||
|
||||
**Stream Quality Presets:**
|
||||
|
||||
| Preset | Camera Resolution | Camera Bitrate | Screen Resolution | Screen Bitrate |
|
||||
|--------|------------------|----------------|-------------------|----------------|
|
||||
| low | 360p | 600 Kbps | 720p@5fps | 1.5 Mbps |
|
||||
| medium | 720p | 1.7 Mbps | 1080p@15fps | 3 Mbps |
|
||||
| high | 1080p | 4 Mbps | 1080p@30fps | 6 Mbps |
|
||||
| source | 1080p | 8 Mbps | native | 10 Mbps |
|
||||
|
||||
### Connection Quality
|
||||
|
||||
A 2-second polling interval collects WebRTC stats from both publisher and subscriber PeerConnections. Quality is color-coded: green (<100ms), yellow (100-200ms), red (>200ms).
|
||||
|
||||
---
|
||||
|
||||
## REST API Client
|
||||
|
||||
Uses `@tauri-apps/plugin-http` fetch (not browser fetch) to bypass self-signed cert rejection. All requests include `danger: { acceptInvalidCerts: true }` for server URLs only. Third-party fetches use standard cert validation.
|
||||
|
||||
---
|
||||
|
||||
## Dispatcher
|
||||
|
||||
`wireDispatcher(ws)` attaches listeners to the WsClient, routing each server message type to the appropriate store actions. Key mappings:
|
||||
|
||||
- `ready` -> sets channels, members, voice states, DM channels
|
||||
- `chat_message` -> adds message, increments unread, triggers notifications
|
||||
- `voice_token` -> starts LiveKit session
|
||||
- `presence` -> updates member status
|
||||
- `server_restart` -> shows warning banner
|
||||
|
||||
See [protocol.md](protocol.md) for complete message type reference.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Contributing
|
||||
|
||||
How to set up the development environment and contribute to OwnCord.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Windows 10+** (x64)
|
||||
- **Go 1.22+** (server)
|
||||
- **Node.js 20+** (client)
|
||||
- **Rust / Cargo** (Tauri client)
|
||||
|
||||
### Available Commands
|
||||
|
||||
#### Server (Go)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `go build -o chatserver.exe -ldflags "-s -w" .` | Build server binary |
|
||||
| `go test ./...` | Run all server tests |
|
||||
| `go test ./... -cover` | Run server tests with coverage |
|
||||
| `go test -race ./...` | Run server tests with race detection |
|
||||
|
||||
#### Client (Tauri v2)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Start Vite dev server with hot reload |
|
||||
| `npm run build` | TypeScript check + Vite production build |
|
||||
| `npm run tauri dev` | Launch Tauri app in dev mode |
|
||||
| `npm run tauri build` | Build release installer |
|
||||
| `npm test` | Run all tests (vitest) |
|
||||
| `npm run test:unit` | Unit tests only |
|
||||
| `npm run test:integration` | Integration tests only |
|
||||
| `npm run test:e2e` | Playwright E2E (mocked Tauri) |
|
||||
| `npm run test:e2e:native` | Playwright E2E (real Tauri exe + CDP) |
|
||||
| `npm run test:e2e:prod` | Playwright E2E (prod build) |
|
||||
| `npm run test:e2e:ui` | Playwright UI mode |
|
||||
| `npm run test:watch` | Vitest watch mode |
|
||||
| `npm run test:coverage` | Coverage report |
|
||||
| `npm run typecheck` | Full typecheck (all sources) |
|
||||
| `npm run lint` | ESLint check (src/) |
|
||||
| `npm run lint:fix` | ESLint auto-fix |
|
||||
|
||||
## Active Branches
|
||||
|
||||
- `main` -- stable releases
|
||||
- `dev` -- active development
|
||||
|
||||
## Branch Naming
|
||||
|
||||
- `feature/<name>` -- new features
|
||||
- `fix/<name>` -- bug fixes
|
||||
- `docs/<name>` -- documentation changes
|
||||
|
||||
## Commit Format
|
||||
|
||||
Use conventional commits:
|
||||
|
||||
```text
|
||||
feat: add thread support to channels
|
||||
fix: prevent duplicate WebSocket connections
|
||||
refactor: extract permission checks into middleware
|
||||
docs: update quick-start guide
|
||||
test: add integration tests for invite flow
|
||||
chore: bump Go dependencies
|
||||
perf: cache role permissions in memory
|
||||
ci: add lint step to GitHub Actions
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Branch from `dev` (the active development branch)
|
||||
2. PRs target `dev`; `main` is for stable releases only
|
||||
3. CI must pass (build + test + lint)
|
||||
4. Request code review
|
||||
5. Squash merge preferred
|
||||
|
||||
## Testing
|
||||
|
||||
Target **80%+ coverage**. Follow test-driven development workflow.
|
||||
|
||||
## Code Style
|
||||
|
||||
- **TypeScript**: See [Client Architecture](client-architecture.md)
|
||||
- **Go**: `gofmt` + `golangci-lint`, standard library preferred
|
||||
- **Rust**: `cargo fmt` + `cargo clippy`, minimal code (native APIs only)
|
||||
@@ -0,0 +1,246 @@
|
||||
# Deployment Guide
|
||||
|
||||
Production deployment guide for OwnCord server on Windows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Windows 10+** (x64)
|
||||
- **Go 1.22+** (only if building from source)
|
||||
- **LiveKit Server** binary (for voice/video) -- see [LiveKit Setup](livekit-setup.md)
|
||||
- Ports available: `8443` (default), `7880` (LiveKit), `80` (if using ACME/Let's Encrypt)
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
cd Server
|
||||
go build -o chatserver.exe -ldflags "-s -w -X main.version=1.0.0" .
|
||||
```
|
||||
|
||||
- `-s -w` strips debug info (smaller binary)
|
||||
- `-X main.version=...` embeds the version string
|
||||
|
||||
Alternatively, download a pre-built `chatserver.exe` from GitHub Releases.
|
||||
|
||||
## First Run Behavior
|
||||
|
||||
When `chatserver.exe` starts for the first time:
|
||||
|
||||
1. **Config creation** -- `config.yaml` is written to the working directory with defaults
|
||||
2. **Data directory** -- `data/` is created (database, certs, uploads, backups)
|
||||
3. **TLS certificate** -- A self-signed certificate is generated at `data/cert.pem` / `data/key.pem`
|
||||
4. **Database migration** -- SQLite database is created and all migrations run
|
||||
5. **Status reset** -- All user statuses are set to `offline`, stale voice states are cleared
|
||||
6. **Admin setup page** -- Navigate to `https://localhost:8443/admin` to create the Owner account
|
||||
|
||||
The server listens on `https://0.0.0.0:8443` by default. See [Server Configuration](server-configuration.md) for all options.
|
||||
|
||||
## Running as a Windows Service
|
||||
|
||||
### Option 1: NSSM (Non-Sucking Service Manager)
|
||||
|
||||
```powershell
|
||||
# Install NSSM (via Chocolatey or download from nssm.cc)
|
||||
choco install nssm
|
||||
|
||||
# Create service
|
||||
nssm install OwnCord "C:\OwnCord\chatserver.exe"
|
||||
nssm set OwnCord AppDirectory "C:\OwnCord"
|
||||
nssm set OwnCord DisplayName "OwnCord Chat Server"
|
||||
nssm set OwnCord Start SERVICE_AUTO_START
|
||||
|
||||
# Manage
|
||||
nssm start OwnCord
|
||||
nssm stop OwnCord
|
||||
nssm restart OwnCord
|
||||
```
|
||||
|
||||
### Option 2: Task Scheduler
|
||||
|
||||
1. Open Task Scheduler, create a new task
|
||||
2. Trigger: **At startup**
|
||||
3. Action: Start `chatserver.exe`
|
||||
4. Set "Start in" to the directory containing `config.yaml`
|
||||
5. Check "Run whether user is logged on or not"
|
||||
6. Check "Run with highest privileges"
|
||||
|
||||
## TLS Setup
|
||||
|
||||
### Self-Signed (default)
|
||||
|
||||
Auto-generated on first run. The Tauri client uses TOFU pinning to accept the cert on first connect.
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
mode: "self_signed"
|
||||
```
|
||||
|
||||
### Let's Encrypt (ACME)
|
||||
|
||||
Automatic certificate issuance and renewal. Requires port 80 open and a public domain.
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
mode: "acme"
|
||||
domain: "chat.example.com"
|
||||
acme_cache_dir: "data/acme_certs"
|
||||
```
|
||||
|
||||
### Manual Certificate
|
||||
|
||||
Use your own certificate files:
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
mode: "manual"
|
||||
cert_file: "path/to/cert.pem"
|
||||
key_file: "path/to/key.pem"
|
||||
```
|
||||
|
||||
### TLS Off
|
||||
|
||||
Not recommended. For development or when behind a TLS-terminating reverse proxy:
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
mode: "off"
|
||||
```
|
||||
|
||||
## Backup Strategy
|
||||
|
||||
### SQLite WAL Considerations
|
||||
|
||||
The database uses SQLite WAL mode. Do NOT copy the `.db` file directly while the server is running -- use the backup endpoint instead.
|
||||
|
||||
### Admin Backup Endpoint
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/admin/api/backups` | POST | Create a new backup |
|
||||
| `/admin/api/backups` | GET | List all backups (newest first) |
|
||||
| `/admin/api/backups/{name}` | DELETE | Delete a backup |
|
||||
| `/admin/api/backups/{name}/restore` | POST | Restore from backup (creates pre-restore safety backup first) |
|
||||
|
||||
Backups are stored in `data/backups/` with timestamps.
|
||||
|
||||
### Scheduled Backups
|
||||
|
||||
Use Windows Task Scheduler with PowerShell:
|
||||
|
||||
```powershell
|
||||
$headers = @{ "Cookie" = "session=<admin-session-token>" }
|
||||
Invoke-RestMethod -Uri "https://localhost:8443/admin/api/backups" -Method POST -Headers $headers -SkipCertificateCheck
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
Restoring replaces the live database file. A pre-restore safety backup is created automatically. A server restart is recommended after restore.
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Endpoint
|
||||
|
||||
`GET /health` -- public, no authentication required.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"uptime": 86400,
|
||||
"online_users": 12
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics Endpoint
|
||||
|
||||
`GET /api/v1/metrics` -- admin IP restricted.
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime": "24h0m0s",
|
||||
"uptime_seconds": 86400,
|
||||
"goroutines": 42,
|
||||
"heap_alloc_mb": 15.3,
|
||||
"heap_sys_mb": 24.0,
|
||||
"num_gc": 150,
|
||||
"connected_users": 12,
|
||||
"voice_sessions": 3,
|
||||
"livekit_healthy": true
|
||||
}
|
||||
```
|
||||
|
||||
### LiveKit Health
|
||||
|
||||
`GET /api/v1/livekit/health` -- checks LiveKit companion process reachability.
|
||||
|
||||
### Diagnostics
|
||||
|
||||
`GET /api/v1/diagnostics/connectivity` -- connectivity diagnostics for troubleshooting.
|
||||
|
||||
## Auto-Update
|
||||
|
||||
### Server
|
||||
|
||||
The server checks GitHub Releases for updates:
|
||||
- Compares semver versions
|
||||
- Results are cached for 1 hour
|
||||
- Downloads `chatserver.exe` with SHA256 checksum verification
|
||||
- On restart, the old binary is cleaned up
|
||||
|
||||
Set `github.token` in config for higher API rate limits (5000/hr vs 60/hr unauthenticated).
|
||||
|
||||
### Client
|
||||
|
||||
The Tauri client uses NSIS installer updates:
|
||||
- Server exposes client update assets from GitHub Releases
|
||||
- Ed25519 signature verification before applying
|
||||
|
||||
## Firewall and Ports
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| `8443` | TCP | HTTPS server (configurable via `server.port`) |
|
||||
| `80` | TCP | ACME HTTP-01 challenge (only if `tls.mode: acme`) |
|
||||
| `7880` | TCP | LiveKit server (WebSocket signaling) |
|
||||
| `7881` | TCP | LiveKit server (RTC/TURN over TCP) |
|
||||
| `50000-60000` | UDP | LiveKit WebRTC media (ICE candidates) |
|
||||
|
||||
For remote access, see the [Port Forwarding Guide](port-forwarding.md) or [Tailscale Guide](tailscale.md).
|
||||
|
||||
## Hardening Checklist
|
||||
|
||||
- [ ] **Change default admin password** -- create a strong Owner password during setup
|
||||
- [ ] **Set `admin_allowed_cidrs`** -- restrict admin access to specific IPs if needed
|
||||
- [ ] **Enable TLS** -- use `acme` or `manual` mode; avoid `off` in production
|
||||
- [ ] **Set `allowed_origins`** -- restrict WebSocket origins to your domain
|
||||
- [ ] **Set `trusted_proxies`** -- configure if behind a reverse proxy
|
||||
- [ ] **Set stable voice credentials** -- set `livekit_api_key` and `livekit_api_secret` to avoid token breakage on restart
|
||||
- [ ] **Set `voice.node_ip`** -- required for remote users behind NAT
|
||||
- [ ] **Review upload limits** -- adjust `upload.max_size_mb` for your use case
|
||||
- [ ] **Configure GitHub token** -- optional, for reliable update checks
|
||||
- [ ] **Schedule backups** -- use the admin backup endpoint on a cron schedule
|
||||
- [ ] **Monitor health** -- poll `/health` for uptime monitoring
|
||||
|
||||
## Background Maintenance
|
||||
|
||||
The server runs a maintenance loop every 15 minutes that:
|
||||
- Purges expired user sessions
|
||||
- Deletes orphaned file attachments (uploaded but never linked to a message, older than 1 hour)
|
||||
- Uses a circuit breaker (pauses after 5 consecutive failures)
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
The server handles `Ctrl+C` (SIGINT) and `SIGTERM`:
|
||||
1. Stops accepting new connections
|
||||
2. Closes all WebSocket connections and voice rooms
|
||||
3. Drains HTTP connections with a 30-second timeout
|
||||
4. Stops the maintenance loop
|
||||
5. Closes the database
|
||||
|
||||
## See Also
|
||||
|
||||
- [Server Configuration](server-configuration.md) -- full config key reference
|
||||
- [LiveKit Setup](livekit-setup.md) -- voice/video setup
|
||||
- [Quick Start](quick-start.md) -- getting started
|
||||
- [Port Forwarding](port-forwarding.md) -- port forwarding for remote access
|
||||
- [Tailscale](tailscale.md) -- zero-config networking
|
||||
- [Security](security.md) -- security guidelines
|
||||
@@ -0,0 +1,132 @@
|
||||
# LiveKit Setup Guide
|
||||
|
||||
LiveKit is an open-source SFU (Selective Forwarding Unit) that handles real-time voice and video. OwnCord uses it instead of rolling its own WebRTC stack -- LiveKit handles all the hard parts (DTLS, ICE, codec negotiation, simulcast) while OwnCord manages permissions, state, and room lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## 1. Get the LiveKit Binary
|
||||
|
||||
Download `livekit-server` for Windows from one of:
|
||||
|
||||
- **GitHub releases**: <https://github.com/livekit/livekit/releases>
|
||||
- Grab the `livekit-server_*_windows_amd64.zip` asset
|
||||
- **LiveKit website**: <https://livekit.io/> (Docs > Self Hosting)
|
||||
|
||||
Extract the binary somewhere permanent (e.g. `C:\livekit\livekit-server.exe`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Server Configuration
|
||||
|
||||
LiveKit settings live in the `voice:` section of `config.yaml`:
|
||||
|
||||
```yaml
|
||||
voice:
|
||||
livekit_api_key: "devkey"
|
||||
livekit_api_secret: "owncord-dev-secret-key-min-32chars"
|
||||
livekit_url: "ws://localhost:7880"
|
||||
livekit_binary: "C:/livekit/livekit-server.exe"
|
||||
quality: "medium"
|
||||
```
|
||||
|
||||
| Field | Purpose | Default |
|
||||
|-------|---------|---------|
|
||||
| `livekit_api_key` | Shared API key between OwnCord and LiveKit | `"devkey"` |
|
||||
| `livekit_api_secret` | Shared secret for JWT signing (min 32 chars) | `"owncord-dev-secret-key-min-32chars"` |
|
||||
| `livekit_url` | LiveKit WebSocket URL | `ws://localhost:7880` |
|
||||
| `livekit_binary` | Path to `livekit-server` binary. Empty = assume externally managed | `""` (disabled) |
|
||||
| `quality` | Default voice quality preset | `"medium"` |
|
||||
|
||||
Environment variable overrides use the `OWNCORD_` prefix: `OWNCORD_VOICE_LIVEKIT_API_KEY`, `OWNCORD_VOICE_LIVEKIT_API_SECRET`, etc.
|
||||
|
||||
> **Warning**: The server logs a warning at startup if you use the default dev key/secret. Always change these for production.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ports and Firewall
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| **7880** | TCP (HTTP/WS) | LiveKit signaling (WebSocket + REST API) |
|
||||
| **7881** | TCP | LiveKit internal RTC (TURN/TCP fallback) |
|
||||
| **50000-60000** | UDP | Media transport (RTP audio/video) |
|
||||
|
||||
For LAN-only setups, ensure these ports are open on Windows Firewall. For remote access, forward these through your router or use [Tailscale](tailscale.md).
|
||||
|
||||
---
|
||||
|
||||
## 4. How the Companion Process Works
|
||||
|
||||
When `livekit_binary` is set, OwnCord manages LiveKit as a companion process:
|
||||
|
||||
1. **Config generation**: OwnCord auto-generates `data/livekit.yaml` with the API key/secret, port 7880, and UDP range 50000-60000
|
||||
2. **Process launch**: `livekit-server --config data/livekit.yaml`
|
||||
3. **Crash recovery**: Exponential backoff restart (3s -> 6s -> 12s ... up to 60s), gives up after 10 consecutive rapid failures
|
||||
4. **Health checks**: `GET http://localhost:7880/` verifies LiveKit is responding
|
||||
5. **Graceful shutdown**: Stops the process when OwnCord shuts down (5s timeout before kill)
|
||||
|
||||
If `livekit_binary` is empty, OwnCord assumes LiveKit is managed externally (e.g. Docker, systemd, or manual start).
|
||||
|
||||
---
|
||||
|
||||
## 5. Token Flow
|
||||
|
||||
How a client joins voice:
|
||||
|
||||
```
|
||||
Client OwnCord Server LiveKit Server
|
||||
| | |
|
||||
|-- voice_join (channel_id)-->| |
|
||||
| |-- check CONNECT_VOICE |
|
||||
| |-- persist to voice_states |
|
||||
| |-- GenerateToken() |
|
||||
|<-- voice_token ------------| |
|
||||
| { token, url, | |
|
||||
| direct_url } | |
|
||||
| | |
|
||||
|-- connect with JWT --------|-------------------------->|
|
||||
|<--- media streams ----------|--------------------------|
|
||||
```
|
||||
|
||||
**Token details:**
|
||||
- Room name: `"channel-{channelID}"`
|
||||
- Identity: `"user-{userID}"`
|
||||
- TTL: 24 hours (refresh at 23h)
|
||||
- `canPublish` is derived from the `SPEAK_VOICE` permission
|
||||
- `canSubscribe` is always true
|
||||
- Client can request refresh via `voice_token_refresh` (rate limited to 1/60s)
|
||||
|
||||
**Client connection paths:**
|
||||
- **Proxy path** (`/livekit`): Client connects through OwnCord's HTTPS server. Avoids mixed-content issues.
|
||||
- **Direct URL** (`ws://localhost:7880`): Used when the client is on localhost.
|
||||
|
||||
---
|
||||
|
||||
## 6. Webhook Integration
|
||||
|
||||
LiveKit sends webhooks to `POST /api/v1/livekit/webhook`. The endpoint verifies the JWT and handles `participant_left` to clean up ghost voice states when a user disconnects from LiveKit without sending a `voice_leave` message.
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| "voice not configured" error | LiveKit client failed to initialize | Check `livekit_api_key` and `livekit_api_secret` are set and secret is >= 32 chars |
|
||||
| "failed to generate voice token" | API key/secret mismatch | Ensure `config.yaml` key/secret match what LiveKit is using |
|
||||
| Voice connects but no audio | Firewall blocking UDP 50000-60000 | Open UDP port range in Windows Firewall |
|
||||
| "backend unavailable" from `/livekit` proxy | LiveKit not running on port 7880 | Check `livekit_binary` path or start LiveKit manually |
|
||||
| "too many rapid failures, giving up" in logs | LiveKit binary crashes on startup | Run `livekit-server --config data/livekit.yaml` manually to see errors |
|
||||
| Mixed content / insecure WS error | Client using direct URL over HTTPS page | Client should use the `/livekit` proxy path |
|
||||
| `GET /api/v1/livekit/health` returns degraded | LiveKit server not reachable | Verify LiveKit is running: `curl http://localhost:7880` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Production Checklist
|
||||
|
||||
- [ ] Change `livekit_api_key` from `"devkey"` to a random string
|
||||
- [ ] Change `livekit_api_secret` to a random 32+ character string
|
||||
- [ ] Open firewall ports: 7880/TCP, 50000-60000/UDP
|
||||
- [ ] If using ACME/manual TLS, ensure LiveKit proxy at `/livekit` is working
|
||||
- [ ] Test voice by joining a voice channel from two clients
|
||||
- [ ] Check `/api/v1/livekit/health` returns `{"status": "ok"}`
|
||||
@@ -0,0 +1,32 @@
|
||||
# Port Forwarding Guide
|
||||
|
||||
How to make your OwnCord server accessible to friends outside your local network.
|
||||
|
||||
## Why
|
||||
|
||||
Friends outside your LAN need a way to reach your server. Port forwarding tells your router to send incoming traffic on a specific port to your server machine.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Find your router's admin page** -- usually `192.168.1.1` or `192.168.0.1`. Check your gateway IP with `ipconfig` (Windows) or `ip route` (Linux).
|
||||
2. **Find the port forwarding section** -- may be listed under "NAT", "Virtual Servers", or "Firewall" depending on your router.
|
||||
3. **Add a rule for the server:**
|
||||
- External port: `8443`
|
||||
- Internal IP: your server machine's local IP
|
||||
- Internal port: `8443`
|
||||
- Protocol: TCP
|
||||
4. **Add a rule for voice chat** (if using voice/video):
|
||||
- External port: `3478`
|
||||
- Internal IP: your server machine's local IP
|
||||
- Internal port: `3478`
|
||||
- Protocol: UDP
|
||||
5. **Find your public IP** at a site like `whatismyip.com`.
|
||||
6. **Share your public IP and port** with friends: `your.public.ip:8443`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Windows Firewall may block incoming connections. `chatserver.exe` should prompt on first run to allow access. If not, manually add a firewall rule for port 8443 (TCP) and 3478 (UDP).
|
||||
|
||||
## Dynamic IP
|
||||
|
||||
If your public IP changes frequently, consider a Dynamic DNS service (e.g., No-IP, DuckDNS) so friends can use a stable hostname instead of a raw IP address.
|
||||
@@ -0,0 +1,838 @@
|
||||
# WebSocket Protocol Reference
|
||||
|
||||
All client-server real-time communication happens over a single WebSocket connection. Messages are JSON with a `type` and `payload`.
|
||||
|
||||
**Related docs:**
|
||||
- [api.md](api.md) -- REST endpoints (message history, file uploads, etc.)
|
||||
- [schema.md](schema.md) -- Database tables and permission bitfields
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Transport Layer](#transport-layer)
|
||||
2. [Message Envelope](#message-envelope)
|
||||
3. [Sequence Numbers](#sequence-numbers)
|
||||
4. [Authentication Flow](#authentication-flow)
|
||||
5. [Heartbeat and Connection Liveness](#heartbeat-and-connection-liveness)
|
||||
6. [Reconnection with State Recovery](#reconnection-with-state-recovery)
|
||||
7. [Initial State (ready)](#initial-state-ready)
|
||||
8. [Chat Messages](#chat-messages)
|
||||
9. [Reactions](#reactions)
|
||||
10. [Typing Indicators](#typing-indicators)
|
||||
11. [Presence](#presence)
|
||||
12. [Channel Focus](#channel-focus)
|
||||
13. [Channel Updates](#channel-updates)
|
||||
14. [Member Updates](#member-updates)
|
||||
15. [Voice Signaling](#voice-signaling)
|
||||
16. [Direct Messages](#direct-messages)
|
||||
17. [Server Restart](#server-restart)
|
||||
18. [Error Handling](#error-handling)
|
||||
19. [Rate Limits](#rate-limits)
|
||||
20. [Message Type Reference Table](#message-type-reference-table)
|
||||
|
||||
---
|
||||
|
||||
## Transport Layer
|
||||
|
||||
### WebSocket Endpoint
|
||||
|
||||
```
|
||||
wss://{host}/api/v1/ws
|
||||
```
|
||||
|
||||
The client connects via the Tauri Rust backend's WS proxy rather than native WebView2 WebSocket. This is required because WebView2 rejects self-signed TLS certificates. The Rust proxy uses TOFU (Trust On First Use) certificate pinning.
|
||||
|
||||
### Transport Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Max read size | 1 MB |
|
||||
| Max message content | 4000 runes |
|
||||
| Write timeout | 10 seconds |
|
||||
| Auth deadline | 10 seconds |
|
||||
| Send buffer per client | 256 messages |
|
||||
|
||||
---
|
||||
|
||||
## Message Envelope
|
||||
|
||||
Every WebSocket message is a JSON object with these fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "message_type",
|
||||
"id": "unique-request-id",
|
||||
"payload": { },
|
||||
"seq": 42
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `type` | string | Yes | Determines how `payload` is interpreted |
|
||||
| `id` | string | Client messages only | Client-generated UUID for request/response correlation |
|
||||
| `payload` | object | Yes | Contents vary by `type`. Must be present (can be `{}`). |
|
||||
| `seq` | uint64 | Broadcast messages only | Monotonically increasing sequence number. Only present on server-to-client broadcast messages. |
|
||||
|
||||
---
|
||||
|
||||
## Sequence Numbers
|
||||
|
||||
The sequence number system enables reconnection with state recovery.
|
||||
|
||||
1. The server maintains an atomic `uint64` counter.
|
||||
2. Every broadcast message gets the next seq number.
|
||||
3. The message is stored in a 1000-event replay ring buffer.
|
||||
4. The client tracks `lastSeq` from every server broadcast.
|
||||
|
||||
### Which Messages Get seq
|
||||
|
||||
| Category | Has seq? | Examples |
|
||||
|----------|----------|---------|
|
||||
| Channel broadcasts | Yes | `chat_message`, `chat_edited`, `chat_deleted`, `reaction_update` |
|
||||
| Global broadcasts | Yes | `presence`, `member_join`, `member_leave`, `member_update`, `member_ban`, `voice_state`, `voice_leave`, `channel_create`, `channel_update`, `channel_delete`, `server_restart` |
|
||||
| Ephemeral | No | `typing` |
|
||||
| DM messages | No | DM `chat_message`, `chat_edited`, `chat_deleted`, `reaction_update`, `dm_channel_open`, `dm_channel_close` |
|
||||
| Direct responses | No | `auth_ok`, `auth_error`, `chat_send_ok`, `error`, `voice_config`, `voice_token`, `pong` |
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### Step 1: Client Sends auth
|
||||
|
||||
After the WebSocket connection is established, the client sends the first message within 10 seconds:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "auth",
|
||||
"payload": {
|
||||
"token": "session-token-from-login",
|
||||
"last_seq": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `token` | string | Yes | Session token obtained from `POST /api/v1/auth/login` |
|
||||
| `last_seq` | uint64 | No | Last sequence number received. If > 0, server attempts replay. Default 0. |
|
||||
|
||||
### Step 2: Success -- auth_ok
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "auth_ok",
|
||||
"payload": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "alex",
|
||||
"avatar": "uuid.png",
|
||||
"role": "admin"
|
||||
},
|
||||
"server_name": "My Server",
|
||||
"motd": "Welcome!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Failure -- auth_error
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "auth_error",
|
||||
"payload": {
|
||||
"message": "Invalid or expired token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After sending `auth_error`, the server closes the connection.
|
||||
|
||||
### Step 4: ready Payload
|
||||
|
||||
After `auth_ok`, the server sends a `ready` message containing all initial state.
|
||||
|
||||
### Step 5: Member Join + Presence
|
||||
|
||||
The server broadcasts to all connected clients:
|
||||
|
||||
```json
|
||||
{ "type": "member_join", "payload": { "user": { "id": 1, "username": "alex", "avatar": "uuid.png", "role": "admin" } } }
|
||||
{ "type": "presence", "payload": { "user_id": 1, "status": "online" } }
|
||||
```
|
||||
|
||||
### Periodic Session Revalidation
|
||||
|
||||
Every 10 messages, the server re-checks the session token against the database. If the session has been revoked, expired, or the user banned, the connection is closed immediately.
|
||||
|
||||
---
|
||||
|
||||
## Heartbeat and Connection Liveness
|
||||
|
||||
### Client Ping
|
||||
|
||||
The client sends a JSON ping every 30 seconds:
|
||||
|
||||
```json
|
||||
{ "type": "ping", "payload": {} }
|
||||
```
|
||||
|
||||
### Server Pong
|
||||
|
||||
The server responds immediately:
|
||||
|
||||
```json
|
||||
{ "type": "pong" }
|
||||
```
|
||||
|
||||
### Server Stale Client Sweep
|
||||
|
||||
Every 30 seconds, the server checks all clients. Any client with no activity for 90 seconds is forcibly disconnected. Normal chat activity also keeps the connection alive.
|
||||
|
||||
---
|
||||
|
||||
## Reconnection with State Recovery
|
||||
|
||||
When a connection drops, the client automatically reconnects with exponential backoff (1s to 30s max) and sends `last_seq` in the `auth` message.
|
||||
|
||||
| Condition | Server Behavior |
|
||||
|-----------|-----------------|
|
||||
| `last_seq == 0` | Full flow: `auth_ok` + `ready` + `member_join` + `presence` |
|
||||
| `last_seq > 0` AND seq in buffer | Replay flow: `auth_ok` + missed events + `presence` (no `member_join`, no `ready`) |
|
||||
| `last_seq > 0` AND seq NOT in buffer | Full flow (fallback): same as `last_seq == 0` |
|
||||
|
||||
DM events are not stored in the ring buffer and are only recoverable via the full `ready` payload.
|
||||
|
||||
---
|
||||
|
||||
## Initial State (ready)
|
||||
|
||||
Sent once after `auth_ok` (fresh connection or replay fallback).
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ready",
|
||||
"payload": {
|
||||
"channels": [ ... ],
|
||||
"dm_channels": [ ... ],
|
||||
"members": [ ... ],
|
||||
"voice_states": [ ... ],
|
||||
"roles": [ ... ],
|
||||
"server_name": "My Server",
|
||||
"motd": "Welcome!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Payload Fields
|
||||
|
||||
**channels[]:** `id`, `name`, `type` (`text`/`voice`/`announcement`), `category`, `position`, `unread_count` (text only), `last_message_id` (text only)
|
||||
|
||||
**dm_channels[]:** `channel_id`, `recipient` (user object with `id`, `username`, `avatar`, `status`), `last_message_id`, `last_message`, `last_message_at`, `unread_count`
|
||||
|
||||
**members[]:** All registered users with `id`, `username`, `avatar`, `role` (lowercase name), `status`
|
||||
|
||||
**voice_states[]:** All users currently in any voice channel: `channel_id`, `user_id`, `muted`, `deafened`
|
||||
|
||||
**roles[]:** All server roles with `id`, `name`, `color`, `permissions` (bitfield)
|
||||
|
||||
---
|
||||
|
||||
## Chat Messages
|
||||
|
||||
### chat_send (Client -> Server)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chat_send",
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"payload": {
|
||||
"channel_id": 5,
|
||||
"content": "Hello everyone!",
|
||||
"reply_to": null,
|
||||
"attachments": ["upload-uuid-1"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| `channel_id` | number | Yes | Positive integer |
|
||||
| `content` | string | Yes* | Max 4000 runes. HTML-sanitized. *Can be empty if `attachments` is non-empty. |
|
||||
| `reply_to` | number or null | No | Message ID being replied to |
|
||||
| `attachments` | string[] | No | Upload IDs from `POST /api/v1/uploads`. Requires `ATTACH_FILES` permission. |
|
||||
|
||||
### chat_send_ok (Server -> Client)
|
||||
|
||||
Direct response to sender (no seq):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chat_send_ok",
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"payload": {
|
||||
"message_id": 1042,
|
||||
"timestamp": "2026-03-14T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### chat_message (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 42,
|
||||
"type": "chat_message",
|
||||
"payload": {
|
||||
"id": 1042,
|
||||
"channel_id": 5,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "alex",
|
||||
"avatar": "uuid.png",
|
||||
"role": "admin"
|
||||
},
|
||||
"content": "Hello everyone!",
|
||||
"reply_to": null,
|
||||
"timestamp": "2026-03-14T10:30:00Z",
|
||||
"attachments": [],
|
||||
"reactions": [],
|
||||
"pinned": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### chat_edit (Client -> Server)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chat_edit",
|
||||
"id": "req-uuid",
|
||||
"payload": {
|
||||
"message_id": 1042,
|
||||
"content": "Hello everyone! (edited)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Own messages only. Max 4000 runes.
|
||||
|
||||
### chat_edited (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 43,
|
||||
"type": "chat_edited",
|
||||
"payload": {
|
||||
"message_id": 1042,
|
||||
"channel_id": 5,
|
||||
"content": "Hello everyone! (edited)",
|
||||
"edited_at": "2026-03-14T10:31:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### chat_delete (Client -> Server)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chat_delete",
|
||||
"id": "req-uuid",
|
||||
"payload": {
|
||||
"message_id": 1042
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Moderators with `MANAGE_MESSAGES` can delete others' messages (non-DM channels only).
|
||||
|
||||
### chat_deleted (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 44,
|
||||
"type": "chat_deleted",
|
||||
"payload": {
|
||||
"message_id": 1042,
|
||||
"channel_id": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reactions
|
||||
|
||||
### reaction_add / reaction_remove (Client -> Server)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "reaction_add",
|
||||
"payload": {
|
||||
"message_id": 1042,
|
||||
"emoji": "\ud83d\udc4d"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rate limited at 5/sec. Requires `ADD_REACTIONS` permission (or DM participant).
|
||||
|
||||
### reaction_update (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 45,
|
||||
"type": "reaction_update",
|
||||
"payload": {
|
||||
"message_id": 1042,
|
||||
"channel_id": 5,
|
||||
"emoji": "\ud83d\udc4d",
|
||||
"user_id": 1,
|
||||
"action": "add"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`action` is `"add"` or `"remove"`.
|
||||
|
||||
---
|
||||
|
||||
## Typing Indicators
|
||||
|
||||
### typing_start (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "typing_start", "payload": { "channel_id": 5 } }
|
||||
```
|
||||
|
||||
Rate limited: 1 per 3 seconds per user per channel. Silently dropped when rate limited.
|
||||
|
||||
### typing (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "typing",
|
||||
"payload": {
|
||||
"channel_id": 5,
|
||||
"user_id": 1,
|
||||
"username": "alex"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Typing broadcasts are ephemeral -- they are NOT stored in the replay ring buffer.
|
||||
|
||||
---
|
||||
|
||||
## Presence
|
||||
|
||||
### presence_update (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "presence_update", "payload": { "status": "online" } }
|
||||
```
|
||||
|
||||
Valid values: `"online"`, `"idle"`, `"dnd"`, `"offline"`. Rate limited: 1 per 10 seconds.
|
||||
|
||||
### presence (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 50,
|
||||
"type": "presence",
|
||||
"payload": {
|
||||
"user_id": 1,
|
||||
"status": "online"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Focus
|
||||
|
||||
### channel_focus (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "channel_focus", "payload": { "channel_id": 5 } }
|
||||
```
|
||||
|
||||
Tells the server which channel the user is currently viewing. Affects broadcast delivery and unread tracking.
|
||||
|
||||
---
|
||||
|
||||
## Channel Updates
|
||||
|
||||
All channel update messages are broadcast to all connected clients. Triggered by REST API calls from admins.
|
||||
|
||||
### channel_create (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 60,
|
||||
"type": "channel_create",
|
||||
"payload": {
|
||||
"id": 8,
|
||||
"name": "gaming",
|
||||
"type": "text",
|
||||
"category": "Hangout",
|
||||
"topic": "",
|
||||
"position": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### channel_update (Server -> Client, broadcast)
|
||||
|
||||
Full channel object (all fields).
|
||||
|
||||
### channel_delete (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 62,
|
||||
"type": "channel_delete",
|
||||
"payload": { "id": 8 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Member Updates
|
||||
|
||||
All member messages are broadcast to all connected clients.
|
||||
|
||||
### member_join (Server -> Client, broadcast)
|
||||
|
||||
Sent when a user first connects (fresh connection, not reconnect replay).
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 70,
|
||||
"type": "member_join",
|
||||
"payload": {
|
||||
"user": {
|
||||
"id": 5,
|
||||
"username": "newuser",
|
||||
"avatar": null,
|
||||
"role": "member"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### member_update (Server -> Client, broadcast)
|
||||
|
||||
Triggered when an admin changes a user's role.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 71,
|
||||
"type": "member_update",
|
||||
"payload": {
|
||||
"user_id": 5,
|
||||
"role": "moderator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### member_ban (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 72,
|
||||
"type": "member_ban",
|
||||
"payload": { "user_id": 5 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Voice Signaling
|
||||
|
||||
Voice uses LiveKit as the SFU. WebSocket messages handle signaling (join/leave/state) while the actual audio/video flows through LiveKit's own WebSocket connection.
|
||||
|
||||
### voice_join (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "voice_join", "payload": { "channel_id": 10 } }
|
||||
```
|
||||
|
||||
On success, server sends (in order):
|
||||
1. `voice_token` -- LiveKit JWT + URL
|
||||
2. `voice_state` broadcast -- joiner's state to all clients
|
||||
3. Existing `voice_state` messages -- one per existing participant (to joiner only)
|
||||
4. `voice_config` -- channel audio settings (to joiner only)
|
||||
|
||||
### voice_token (Server -> Client, direct)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "voice_token",
|
||||
"payload": {
|
||||
"channel_id": 10,
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"url": "/livekit",
|
||||
"direct_url": "ws://localhost:7880"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### voice_config (Server -> Client, direct)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "voice_config",
|
||||
"payload": {
|
||||
"channel_id": 10,
|
||||
"quality": "medium",
|
||||
"bitrate": 64000,
|
||||
"max_users": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Quality presets:
|
||||
|
||||
| Preset | Bitrate |
|
||||
|--------|---------|
|
||||
| `low` | 32,000 bps |
|
||||
| `medium` | 64,000 bps |
|
||||
| `high` | 128,000 bps |
|
||||
|
||||
### voice_leave (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "voice_leave", "payload": {} }
|
||||
```
|
||||
|
||||
### voice_leave (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 80,
|
||||
"type": "voice_leave",
|
||||
"payload": {
|
||||
"channel_id": 10,
|
||||
"user_id": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### voice_state (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 81,
|
||||
"type": "voice_state",
|
||||
"payload": {
|
||||
"channel_id": 10,
|
||||
"user_id": 1,
|
||||
"username": "alex",
|
||||
"muted": false,
|
||||
"deafened": false,
|
||||
"speaking": false,
|
||||
"camera": false,
|
||||
"screenshare": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### voice_mute / voice_deafen (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "voice_mute", "payload": { "muted": true } }
|
||||
{ "type": "voice_deafen", "payload": { "deafened": true } }
|
||||
```
|
||||
|
||||
### voice_camera (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "voice_camera", "payload": { "enabled": true } }
|
||||
```
|
||||
|
||||
Rate limited: 2/sec. Requires `USE_VIDEO` permission.
|
||||
|
||||
### voice_screenshare (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "voice_screenshare", "payload": { "enabled": true } }
|
||||
```
|
||||
|
||||
Rate limited: 2/sec. Requires `SHARE_SCREEN` permission.
|
||||
|
||||
### voice_token_refresh (Client -> Server)
|
||||
|
||||
```json
|
||||
{ "type": "voice_token_refresh", "payload": {} }
|
||||
```
|
||||
|
||||
Rate limited: 1 per 60 seconds. Must be in a voice channel.
|
||||
|
||||
---
|
||||
|
||||
## Direct Messages
|
||||
|
||||
### dm_channel_open (Server -> Client)
|
||||
|
||||
Sent when a DM is opened, created, or auto-reopened by an incoming message.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "dm_channel_open",
|
||||
"payload": {
|
||||
"channel_id": 100,
|
||||
"recipient": {
|
||||
"id": 2,
|
||||
"username": "jordan",
|
||||
"avatar": "uuid.png",
|
||||
"status": "online"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### dm_channel_close (Server -> Client)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "dm_channel_close",
|
||||
"payload": { "channel_id": 100 }
|
||||
}
|
||||
```
|
||||
|
||||
### DM Authorization
|
||||
|
||||
All handlers that touch a channel check the channel type and branch to participant-based authorization for DMs instead of role-based permissions. This applies to: `chat_send`, `chat_edit`, `chat_delete`, `reaction_add`/`remove`, `typing_start`, `channel_focus`.
|
||||
|
||||
---
|
||||
|
||||
## Server Restart
|
||||
|
||||
### server_restart (Server -> Client, broadcast)
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 100,
|
||||
"type": "server_restart",
|
||||
"payload": {
|
||||
"reason": "update",
|
||||
"delay_seconds": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### error (Server -> Client)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"id": "original-req-uuid",
|
||||
"payload": {
|
||||
"code": "FORBIDDEN",
|
||||
"message": "No permission to post here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `BAD_REQUEST` | Invalid payload format or field values |
|
||||
| `INTERNAL` | Server-side error |
|
||||
| `NOT_FOUND` | Channel or message not found |
|
||||
| `FORBIDDEN` | Missing required permission |
|
||||
| `RATE_LIMITED` | Too many requests (includes `retry_after` in seconds) |
|
||||
| `ALREADY_JOINED` | Already in this voice channel |
|
||||
| `CHANNEL_FULL` | Voice channel at capacity |
|
||||
| `VOICE_ERROR` | Voice-specific error |
|
||||
| `VIDEO_LIMIT` | Maximum video streams reached |
|
||||
| `BANNED` | User is banned |
|
||||
| `INVALID_JSON` | Message is not valid JSON |
|
||||
| `UNKNOWN_TYPE` | Unrecognized message type |
|
||||
| `SLOW_MODE` | Channel has slow mode enabled |
|
||||
| `CONFLICT` | Duplicate reaction or constraint violation |
|
||||
|
||||
After 10 consecutive invalid JSON messages, the connection is forcibly closed.
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
All rate limits are enforced server-side using a token bucket rate limiter.
|
||||
|
||||
| Action | Limit | Window | Error Response |
|
||||
|--------|-------|--------|----------------|
|
||||
| Chat send | 10 | 1 second | `RATE_LIMITED` error |
|
||||
| Chat edit | 10 | 1 second | `RATE_LIMITED` error |
|
||||
| Chat delete | 10 | 1 second | `RATE_LIMITED` error |
|
||||
| Typing | 1 | 3 seconds | Silently dropped |
|
||||
| Presence | 1 | 10 seconds | `RATE_LIMITED` error |
|
||||
| Reactions | 5 | 1 second | `RATE_LIMITED` error |
|
||||
| Voice camera | 2 | 1 second | `RATE_LIMITED` error |
|
||||
| Voice screenshare | 2 | 1 second | `RATE_LIMITED` error |
|
||||
| Voice token refresh | 1 | 60 seconds | `RATE_LIMITED` error |
|
||||
|
||||
---
|
||||
|
||||
## Message Type Reference Table
|
||||
|
||||
### Client -> Server (18 types)
|
||||
|
||||
| Type | Rate Limit | Notes |
|
||||
|------|-----------|-------|
|
||||
| `auth` | N/A (first message) | Token + optional last_seq |
|
||||
| `chat_send` | 10/sec | + slow mode per channel |
|
||||
| `chat_edit` | 10/sec | Own messages only |
|
||||
| `chat_delete` | 10/sec | Own or mod (non-DM) |
|
||||
| `reaction_add` | 5/sec | |
|
||||
| `reaction_remove` | 5/sec | |
|
||||
| `typing_start` | 1/3sec/channel | Silently dropped |
|
||||
| `channel_focus` | None | Updates read state |
|
||||
| `presence_update` | 1/10sec | |
|
||||
| `voice_join` | None | |
|
||||
| `voice_leave` | None | Empty payload |
|
||||
| `voice_mute` | None | |
|
||||
| `voice_deafen` | None | |
|
||||
| `voice_camera` | 2/sec | Requires USE_VIDEO |
|
||||
| `voice_screenshare` | 2/sec | Requires SHARE_SCREEN |
|
||||
| `voice_token_refresh` | 1/60sec | Must be in voice |
|
||||
| `soundboard_play` | N/A | Not yet implemented server-side |
|
||||
| `ping` | None | Heartbeat |
|
||||
|
||||
### Server -> Client (25+ types)
|
||||
|
||||
| Type | Has seq? | Delivery |
|
||||
|------|----------|----------|
|
||||
| `auth_ok` | No | Direct |
|
||||
| `auth_error` | No | Direct (then close) |
|
||||
| `ready` | No | Direct |
|
||||
| `chat_message` | Non-DM only | Channel or DM participants |
|
||||
| `chat_send_ok` | No | Direct to sender |
|
||||
| `chat_edited` | Non-DM only | Channel or DM participants |
|
||||
| `chat_deleted` | Non-DM only | Channel or DM participants |
|
||||
| `reaction_update` | Non-DM only | Channel or DM participants |
|
||||
| `typing` | No | Channel (excl. sender) or DM |
|
||||
| `presence` | Yes | All clients |
|
||||
| `channel_create` | Yes | All clients |
|
||||
| `channel_update` | Yes | All clients |
|
||||
| `channel_delete` | Yes | All clients |
|
||||
| `voice_state` | Yes | All clients |
|
||||
| `voice_leave` | Yes | All clients |
|
||||
| `voice_config` | No | Direct to joiner |
|
||||
| `voice_token` | No | Direct to joiner |
|
||||
| `member_join` | Yes | All clients |
|
||||
| `member_update` | Yes | All clients |
|
||||
| `member_ban` | Yes | All clients |
|
||||
| `dm_channel_open` | No | Direct to participant |
|
||||
| `dm_channel_close` | No | Direct to participant |
|
||||
| `server_restart` | Yes | All clients |
|
||||
| `error` | No | Direct to requester |
|
||||
| `pong` | No | Direct to pinger |
|
||||
@@ -0,0 +1,66 @@
|
||||
# Quick Start Guide
|
||||
|
||||
Get OwnCord up and running in minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Windows 10+** (x64)
|
||||
- **Go 1.25+** (only if building the server from source)
|
||||
- **Node.js 20+** (only if building the client from source)
|
||||
- **Rust / Cargo** (only if building the Tauri client from source)
|
||||
- **LiveKit Server** binary (optional, for voice/video) -- see [LiveKit Setup](livekit-setup.md)
|
||||
|
||||
## Step 1: Download
|
||||
|
||||
Get the latest release from GitHub Releases. Download `chatserver.exe` and the `OwnCord` installer.
|
||||
|
||||
Or build from source:
|
||||
|
||||
```bash
|
||||
# Server
|
||||
cd Server
|
||||
go build -o chatserver.exe -ldflags "-s -w -X main.version=1.0.0" .
|
||||
|
||||
# Client
|
||||
cd Client/tauri-client
|
||||
npm install
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
## Step 2: Run the Server
|
||||
|
||||
Run `chatserver.exe`. On first run:
|
||||
|
||||
1. `config.yaml` is created in the working directory with default settings
|
||||
2. `data/` directory is created for the database, TLS certs, uploads, and backups
|
||||
3. A self-signed TLS certificate is generated automatically
|
||||
4. SQLite database is created and all migrations are applied
|
||||
5. All user statuses are reset to offline (clean slate)
|
||||
|
||||
The server starts on `https://0.0.0.0:8443`.
|
||||
|
||||
See [Server Configuration](server-configuration.md) for the full config key reference and environment variable overrides.
|
||||
|
||||
## Step 3: Admin Setup
|
||||
|
||||
Open `https://localhost:8443/admin` in a browser. The first-run setup page will prompt you to create the Owner account (username + password). This user gets the Owner role with full server control.
|
||||
|
||||
## Step 4: Create Invites
|
||||
|
||||
In the admin panel, go to invite management and generate invite codes for your friends.
|
||||
|
||||
## Step 5: Connect Clients
|
||||
|
||||
Friends install OwnCord, enter your server address (IP or domain + port 8443), and redeem their invite code to register.
|
||||
|
||||
The client uses TOFU (Trust On First Use) for self-signed certificates -- it will prompt to trust the server's certificate on first connection, then pin it for future sessions.
|
||||
|
||||
## Networking
|
||||
|
||||
If friends are outside your local network, see the [Port Forwarding Guide](port-forwarding.md) or use [Tailscale](tailscale.md) for zero-config networking.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Server Configuration](server-configuration.md) -- customize ports, TLS, uploads, voice
|
||||
- [Deployment Guide](deployment.md) -- production hardening, backups, monitoring, Windows service setup
|
||||
- [LiveKit Setup](livekit-setup.md) -- enable voice and video chat
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
# Database Schema Reference
|
||||
|
||||
OwnCord uses a single SQLite database file (`data/chatserver.db`) with the pure-Go driver `modernc.org/sqlite` (no CGO). Migrations run automatically on startup.
|
||||
|
||||
---
|
||||
|
||||
## Database Configuration
|
||||
|
||||
| PRAGMA | Value | Purpose |
|
||||
|--------|-------|---------|
|
||||
| `journal_mode` | `WAL` | Write-Ahead Logging for concurrent readers |
|
||||
| `foreign_keys` | `ON` | Enforces all `REFERENCES` constraints |
|
||||
| `busy_timeout` | `5000` | Waits up to 5 seconds for the write lock |
|
||||
| `synchronous` | `NORMAL` | Safe with WAL mode, reduces fsync calls |
|
||||
| `temp_store` | `MEMORY` | Temporary tables stored in RAM |
|
||||
| `mmap_size` | `268435456` | 256 MB memory-mapped I/O |
|
||||
| `cache_size` | `-64000` | 64 MB page cache |
|
||||
|
||||
SQLite only allows one writer at a time. The connection pool is pinned to a single connection.
|
||||
|
||||
---
|
||||
|
||||
## Migration System
|
||||
|
||||
Migrations are embedded `.sql` files applied in lexicographic order. Each migration runs in a transaction and is tracked in `schema_versions`.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
### Migration History
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `001_initial_schema.sql` | All core tables, default roles and settings |
|
||||
| `002_voice_states.sql` | Adds `voice_states` table |
|
||||
| `003_audit_log.sql` | Recreates `audit_log` with renamed columns |
|
||||
| `003_voice_optimization.sql` | Adds `camera`, `screenshare` to voice_states; voice settings to channels |
|
||||
| `004_fix_member_permissions.sql` | Fixes Member role permissions |
|
||||
| `005_channel_overrides_index.sql` | Adds composite index on channel_overrides |
|
||||
| `006_member_video_permissions.sql` | Adds USE_VIDEO and SHARE_SCREEN to Member role |
|
||||
| `007_attachment_dimensions.sql` | Adds `width` and `height` to attachments |
|
||||
| `008_dm_tables.sql` | Adds `dm_participants` and `dm_open_state` tables |
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
### roles
|
||||
|
||||
Defines permission tiers.
|
||||
|
||||
```sql
|
||||
CREATE TABLE roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
color TEXT,
|
||||
permissions INTEGER NOT NULL DEFAULT 0,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
is_default INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
**Default roles:**
|
||||
|
||||
| id | name | color | permissions | position | Notes |
|
||||
|----|------|-------|-------------|----------|-------|
|
||||
| 1 | Owner | `#E74C3C` | `0x7FFFFFFF` | 100 | All 31 permission bits set |
|
||||
| 2 | Admin | `#F39C12` | `0x3FFFFFFF` | 80 | Everything except ADMINISTRATOR |
|
||||
| 3 | Moderator | `#3498DB` | `0x000FFFFF` | 60 | All message + voice + moderation |
|
||||
| 4 | Member | NULL | `0x1E63` | 40 | Send, read, attach, react, voice, video, screen share |
|
||||
|
||||
---
|
||||
|
||||
### users
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
password TEXT NOT NULL,
|
||||
avatar TEXT,
|
||||
role_id INTEGER NOT NULL DEFAULT 4 REFERENCES roles(id),
|
||||
totp_secret TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'offline',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_seen TEXT,
|
||||
banned INTEGER NOT NULL DEFAULT 0,
|
||||
ban_reason TEXT,
|
||||
ban_expires TEXT
|
||||
);
|
||||
```
|
||||
|
||||
Valid status values: `online`, `idle`, `dnd`, `offline`. All statuses are reset to `offline` on server startup.
|
||||
|
||||
---
|
||||
|
||||
### sessions
|
||||
|
||||
```sql
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
device TEXT,
|
||||
ip_address TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_used TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Session TTL: 30 days. Token is stored as SHA-256 hash.
|
||||
|
||||
---
|
||||
|
||||
### channels
|
||||
|
||||
```sql
|
||||
CREATE TABLE channels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'text',
|
||||
category TEXT,
|
||||
topic TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
slow_mode INTEGER NOT NULL DEFAULT 0,
|
||||
archived INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
voice_max_users INTEGER NOT NULL DEFAULT 0,
|
||||
voice_quality TEXT,
|
||||
mixing_threshold INTEGER,
|
||||
voice_max_video INTEGER NOT NULL DEFAULT 25
|
||||
);
|
||||
```
|
||||
|
||||
Channel types: `text`, `voice`, `announcement`, `dm`.
|
||||
|
||||
---
|
||||
|
||||
### channel_overrides
|
||||
|
||||
Per-channel permission overrides for specific roles.
|
||||
|
||||
```sql
|
||||
CREATE TABLE channel_overrides (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
allow INTEGER NOT NULL DEFAULT 0,
|
||||
deny INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(channel_id, role_id)
|
||||
);
|
||||
```
|
||||
|
||||
Effective permission calculation: `effective = (base_permissions & ~deny) | allow`
|
||||
|
||||
---
|
||||
|
||||
### messages
|
||||
|
||||
```sql
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
content TEXT NOT NULL,
|
||||
reply_to INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
||||
edited_at TEXT,
|
||||
deleted INTEGER NOT NULL DEFAULT 0,
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
Messages are soft-deleted (`deleted = 1`), never physically removed by user action.
|
||||
|
||||
---
|
||||
|
||||
### messages_fts (FTS5 Virtual Table)
|
||||
|
||||
Full-text search index synchronized via triggers.
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
||||
content,
|
||||
content='messages',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
Supports FTS5 query syntax: simple terms, phrase queries, prefix queries, boolean operators (`AND`, `OR`, `NOT`).
|
||||
|
||||
---
|
||||
|
||||
### attachments
|
||||
|
||||
```sql
|
||||
CREATE TABLE attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
stored_as TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
width INTEGER,
|
||||
height INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
Uses UUID primary keys. `message_id` is NULL during upload, linked when the message is sent.
|
||||
|
||||
---
|
||||
|
||||
### reactions
|
||||
|
||||
```sql
|
||||
CREATE TABLE reactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
emoji TEXT NOT NULL,
|
||||
UNIQUE(message_id, user_id, emoji)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### invites
|
||||
|
||||
```sql
|
||||
CREATE TABLE invites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||||
redeemed_by INTEGER REFERENCES users(id),
|
||||
max_uses INTEGER,
|
||||
use_count INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
revoked INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
Invite codes are 8 random bytes encoded as hex. Uses are validated and incremented atomically.
|
||||
|
||||
---
|
||||
|
||||
### read_states
|
||||
|
||||
```sql
|
||||
CREATE TABLE read_states (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
last_message_id INTEGER NOT NULL DEFAULT 0,
|
||||
mention_count INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, channel_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### audit_log
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor_id INTEGER NOT NULL DEFAULT 0,
|
||||
action TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL DEFAULT '',
|
||||
target_id INTEGER NOT NULL DEFAULT 0,
|
||||
detail TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### voice_states
|
||||
|
||||
Ephemeral -- all rows deleted on server startup.
|
||||
|
||||
```sql
|
||||
CREATE TABLE voice_states (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
muted INTEGER NOT NULL DEFAULT 0,
|
||||
deafened INTEGER NOT NULL DEFAULT 0,
|
||||
speaking INTEGER NOT NULL DEFAULT 0,
|
||||
camera INTEGER NOT NULL DEFAULT 0,
|
||||
screenshare INTEGER NOT NULL DEFAULT 0,
|
||||
joined_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### dm_participants
|
||||
|
||||
```sql
|
||||
CREATE TABLE dm_participants (
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (channel_id, user_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### dm_open_state
|
||||
|
||||
```sql
|
||||
CREATE TABLE dm_open_state (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
opened_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (user_id, channel_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Indexes
|
||||
|
||||
| Index Name | Table | Columns | Purpose |
|
||||
|------------|-------|---------|---------|
|
||||
| `idx_sessions_token` | sessions | `(token)` | Fast session lookup by token hash |
|
||||
| `idx_sessions_user` | sessions | `(user_id)` | Fast deletion of all sessions for a user |
|
||||
| `idx_messages_channel` | messages | `(channel_id, id DESC)` | Latest messages in channel query |
|
||||
| `idx_messages_user` | messages | `(user_id)` | Filter by author |
|
||||
| `idx_invites_code` | invites | `(code)` | Fast invite validation |
|
||||
| `idx_audit_timestamp` | audit_log | `(created_at DESC)` | Pagination of audit log |
|
||||
| `idx_audit_log_actor` | audit_log | `(actor_id)` | Filter by actor |
|
||||
| `idx_login_ip` | login_attempts | `(ip_address, timestamp)` | Rate limiting queries |
|
||||
| `idx_voice_states_channel` | voice_states | `(channel_id)` | All users in a voice channel |
|
||||
| `idx_channel_overrides_channel_role` | channel_overrides | `(channel_id, role_id)` | Permission lookup |
|
||||
| `idx_dm_participants_user` | dm_participants | `(user_id)` | DM channel lookup |
|
||||
|
||||
---
|
||||
|
||||
## Permission Bitfield System
|
||||
|
||||
Permissions are stored as an integer bitfield (31 bits used) in `roles.permissions`, `channel_overrides.allow`, and `channel_overrides.deny`.
|
||||
|
||||
### Bit Map
|
||||
|
||||
| Bit | Hex | Name | Description |
|
||||
|-----|-----|------|-------------|
|
||||
| 0 | `0x1` | `SEND_MESSAGES` | Post messages in text channels |
|
||||
| 1 | `0x2` | `READ_MESSAGES` | View messages in text channels |
|
||||
| 5 | `0x20` | `ATTACH_FILES` | Upload file attachments |
|
||||
| 6 | `0x40` | `ADD_REACTIONS` | Add emoji reactions |
|
||||
| 8 | `0x100` | `USE_SOUNDBOARD` | Play sounds in voice channels |
|
||||
| 9 | `0x200` | `CONNECT_VOICE` | Join voice channels |
|
||||
| 10 | `0x400` | `SPEAK_VOICE` | Transmit audio in voice channels |
|
||||
| 11 | `0x800` | `USE_VIDEO` | Enable camera in voice channels |
|
||||
| 12 | `0x1000` | `SHARE_SCREEN` | Share screen in voice channels |
|
||||
| 16 | `0x10000` | `MANAGE_MESSAGES` | Delete others' messages, pin/unpin |
|
||||
| 17 | `0x20000` | `MANAGE_CHANNELS` | Create, edit, delete channels |
|
||||
| 18 | `0x40000` | `KICK_MEMBERS` | Kick users |
|
||||
| 19 | `0x80000` | `BAN_MEMBERS` | Ban/unban users |
|
||||
| 20 | `0x100000` | `MUTE_MEMBERS` | Server-side mute/deafen in voice |
|
||||
| 24 | `0x1000000` | `MANAGE_ROLES` | Create, edit, delete roles |
|
||||
| 25 | `0x2000000` | `MANAGE_SERVER` | Modify server settings |
|
||||
| 26 | `0x4000000` | `MANAGE_INVITES` | Create and revoke invite codes |
|
||||
| 27 | `0x8000000` | `VIEW_AUDIT_LOG` | View the audit log |
|
||||
| 30 | `0x40000000` | `ADMINISTRATOR` | Bypasses ALL permission checks |
|
||||
|
||||
Bits 2-4, 7, 13-15, 21-23, 28-29, 31 are reserved.
|
||||
|
||||
### Permission Checking Logic
|
||||
|
||||
```
|
||||
1. Get the user's role -> role.Permissions (base)
|
||||
2. If (base & ADMINISTRATOR) != 0 -> ALLOW everything
|
||||
3. Get channel_overrides for (channel_id, role_id) -> allow, deny
|
||||
4. effective = (base | allow) & ~deny
|
||||
5. Check: (effective & required_permission) != 0
|
||||
```
|
||||
|
||||
DM channels bypass role permissions entirely and use participant-based authorization instead.
|
||||
|
||||
### Default Role Permission Values
|
||||
|
||||
| Role | Hex | Permissions |
|
||||
|------|-----|-------------|
|
||||
| Owner | `0x7FFFFFFF` | Everything including ADMINISTRATOR |
|
||||
| Admin | `0x3FFFFFFF` | Everything except ADMINISTRATOR |
|
||||
| Moderator | `0x000FFFFF` | All message + voice + moderation |
|
||||
| Member | `0x1E63` | Send, read, attach, react, voice, video, screen share |
|
||||
@@ -0,0 +1,41 @@
|
||||
# Security Policy
|
||||
|
||||
Security guidelines and vulnerability reporting for OwnCord.
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
Use GitHub Security Advisories to report vulnerabilities: go to Settings > Security > Advisories and create a new advisory.
|
||||
|
||||
**Do NOT open public issues for security bugs.**
|
||||
|
||||
## Response Timeline
|
||||
|
||||
- **Acknowledgment:** Within 48 hours
|
||||
- **Critical fixes:** Within 7 days
|
||||
- **Non-critical fixes:** Included in the next release
|
||||
|
||||
## Two-Factor Authentication
|
||||
|
||||
OwnCord supports TOTP-based 2FA:
|
||||
|
||||
- Users enroll via Settings > Account (QR code + backup codes)
|
||||
- Admins can enforce server-wide 2FA via the `require_2fa` setting in the admin panel
|
||||
- `require_2fa` requires all users to have 2FA enabled and registration to be closed
|
||||
- Login flow returns `requires_2fa: true` with a `partial_token` (10-min TTL, 5-attempt limit)
|
||||
- Auth challenges are rate-limited to 10 req/min per IP
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- No code signing yet -- binaries are verified via SHA256 checksums only
|
||||
|
||||
## Security Hardening Checklist for Operators
|
||||
|
||||
- [ ] Enable TLS (self-signed is the default; custom certs recommended for production)
|
||||
- [ ] Keep invite-only registration enabled (default)
|
||||
- [ ] Set a strong admin password
|
||||
- [ ] Configure rate limits (defaults are sensible but review for your use case)
|
||||
- [ ] Run regular backups via the admin panel
|
||||
- [ ] Keep the server updated (admin panel shows available updates)
|
||||
- [ ] Firewall: only expose port 8443 (HTTPS) and 7880 (LiveKit WebSocket for voice/video)
|
||||
- [ ] Enable server-wide 2FA requirement once all users have enrolled
|
||||
- [ ] Set `admin_allowed_cidrs` to restrict admin panel access to trusted networks
|
||||
@@ -0,0 +1,141 @@
|
||||
# Server Configuration Reference
|
||||
|
||||
Complete reference for all OwnCord server configuration options.
|
||||
|
||||
## Overview
|
||||
|
||||
OwnCord server reads configuration from `config.yaml` in the working directory. On first run, if the file does not exist, a default `config.yaml` is created automatically.
|
||||
|
||||
Configuration is loaded in three layers (later layers override earlier ones):
|
||||
|
||||
1. **Built-in defaults** (compiled into the binary)
|
||||
2. **YAML file** (`config.yaml`)
|
||||
3. **Environment variables** (prefix: `OWNCORD_`)
|
||||
|
||||
## Config Key Reference
|
||||
|
||||
### Server (`server`)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `server.port` | int | `8443` | HTTP(S) listen port |
|
||||
| `server.name` | string | `"OwnCord Server"` | Server display name (shown in `/api/v1/info` and admin panel) |
|
||||
| `server.data_dir` | string | `"data"` | Directory for database, certs, uploads, backups |
|
||||
| `server.allowed_origins` | string[] | `["*"]` | WebSocket CORS allowed origins; restrict in production |
|
||||
| `server.trusted_proxies` | string[] | `[]` | CIDRs of trusted reverse proxies (for X-Forwarded-For) |
|
||||
| `server.admin_allowed_cidrs` | string[] | private networks | CIDRs allowed to access `/admin` routes. Default: `127.0.0.0/8`, `::1/128`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7` |
|
||||
|
||||
### TLS (`tls`)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `tls.mode` | string | `"self_signed"` | TLS mode: `self_signed`, `acme`, `manual`, `off` |
|
||||
| `tls.cert_file` | string | `"data/cert.pem"` | Path to TLS certificate (used by `manual` and `self_signed`) |
|
||||
| `tls.key_file` | string | `"data/key.pem"` | Path to TLS private key |
|
||||
| `tls.domain` | string | `""` | Domain for ACME/Let's Encrypt (required when `mode: acme`) |
|
||||
| `tls.acme_cache_dir` | string | `"data/acme_certs"` | Directory for cached Let's Encrypt certificates |
|
||||
|
||||
### Database (`database`)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `database.path` | string | `"data/chatserver.db"` | Path to SQLite database file |
|
||||
|
||||
### Uploads (`upload`)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `upload.max_size_mb` | int | `100` | Maximum file upload size in megabytes |
|
||||
| `upload.storage_dir` | string | `"data/uploads"` | Directory where uploaded files are stored |
|
||||
|
||||
### Voice / LiveKit (`voice`)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `voice.livekit_api_key` | string | *(random per run)* | LiveKit API key. Set a stable value for persistent voice tokens. |
|
||||
| `voice.livekit_api_secret` | string | *(random per run)* | LiveKit API secret (min 32 chars). Set a stable value for persistent tokens. |
|
||||
| `voice.livekit_url` | string | `"ws://localhost:7880"` | LiveKit server WebSocket URL |
|
||||
| `voice.livekit_binary` | string | `""` | Path to `livekit-server` binary; empty = don't auto-start |
|
||||
| `voice.node_ip` | string | `""` | Public IP for WebRTC ICE candidates; empty = auto-detect. Required for remote users behind NAT. |
|
||||
| `voice.quality` | string | `"medium"` | Voice quality preset: `low`, `medium`, `high` |
|
||||
|
||||
> **Warning:** If `livekit_api_key` or `livekit_api_secret` are left empty, random credentials are generated on each startup. This means voice tokens break on restart. Always set stable credentials in production. See [LiveKit Setup](livekit-setup.md) for details.
|
||||
|
||||
### GitHub / Updates (`github`)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `github.token` | string | `""` | Optional GitHub API token for higher rate limits on update checks (5000 req/hr vs 60) |
|
||||
|
||||
## Environment Variable Overrides
|
||||
|
||||
Every config key can be overridden via environment variables using the prefix `OWNCORD_`.
|
||||
|
||||
**Format:** `OWNCORD_<SECTION>_<KEY>`
|
||||
|
||||
| Environment Variable | Config Path |
|
||||
|---------------------|-------------|
|
||||
| `OWNCORD_SERVER_PORT` | `server.port` |
|
||||
| `OWNCORD_SERVER_NAME` | `server.name` |
|
||||
| `OWNCORD_SERVER_DATA_DIR` | `server.data_dir` |
|
||||
| `OWNCORD_DATABASE_PATH` | `database.path` |
|
||||
| `OWNCORD_TLS_MODE` | `tls.mode` |
|
||||
| `OWNCORD_TLS_CERT_FILE` | `tls.cert_file` |
|
||||
| `OWNCORD_TLS_DOMAIN` | `tls.domain` |
|
||||
| `OWNCORD_UPLOAD_MAX_SIZE_MB` | `upload.max_size_mb` |
|
||||
| `OWNCORD_UPLOAD_STORAGE_DIR` | `upload.storage_dir` |
|
||||
| `OWNCORD_VOICE_LIVEKIT_API_KEY` | `voice.livekit_api_key` |
|
||||
| `OWNCORD_VOICE_LIVEKIT_API_SECRET` | `voice.livekit_api_secret` |
|
||||
| `OWNCORD_VOICE_LIVEKIT_URL` | `voice.livekit_url` |
|
||||
| `OWNCORD_VOICE_NODE_IP` | `voice.node_ip` |
|
||||
| `OWNCORD_VOICE_QUALITY` | `voice.quality` |
|
||||
| `OWNCORD_GITHUB_TOKEN` | `github.token` |
|
||||
|
||||
## Example config.yaml
|
||||
|
||||
```yaml
|
||||
# OwnCord Server Configuration
|
||||
server:
|
||||
port: 8443
|
||||
name: "OwnCord Server"
|
||||
data_dir: "data"
|
||||
allowed_origins: ["*"] # restrict in production
|
||||
trusted_proxies: [] # e.g. ["10.0.0.0/8"] if behind a reverse proxy
|
||||
admin_allowed_cidrs:
|
||||
- "127.0.0.0/8"
|
||||
- "::1/128"
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
|
||||
database:
|
||||
path: "data/chatserver.db"
|
||||
|
||||
tls:
|
||||
mode: "self_signed" # self_signed | acme | manual | off
|
||||
cert_file: "data/cert.pem"
|
||||
key_file: "data/key.pem"
|
||||
domain: "" # required for acme mode
|
||||
acme_cache_dir: "data/acme_certs"
|
||||
|
||||
upload:
|
||||
max_size_mb: 100
|
||||
storage_dir: "data/uploads"
|
||||
|
||||
voice:
|
||||
livekit_api_key: "your-api-key"
|
||||
livekit_api_secret: "your-secret-at-least-32-characters-long"
|
||||
livekit_url: "ws://localhost:7880"
|
||||
livekit_binary: "" # path to livekit-server binary
|
||||
node_ip: "" # public IP for remote users behind NAT
|
||||
quality: "medium" # low | medium | high
|
||||
|
||||
github:
|
||||
token: "" # optional GitHub PAT for update check rate limits
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Deployment Guide](deployment.md) -- production deployment guide
|
||||
- [LiveKit Setup](livekit-setup.md) -- voice/video setup
|
||||
- [Quick Start](quick-start.md) -- getting started
|
||||
@@ -0,0 +1,23 @@
|
||||
# Tailscale Guide (Zero-Config Alternative)
|
||||
|
||||
Use Tailscale for secure, zero-config networking without port forwarding.
|
||||
|
||||
## What is Tailscale
|
||||
|
||||
Tailscale is a mesh VPN that creates encrypted tunnels between your devices using WireGuard. No port forwarding, no dynamic DNS, and it works behind CGNAT. Free for personal use.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Install Tailscale** on the server machine and each client machine: https://tailscale.com/download
|
||||
2. **Sign in** with the same Tailscale account (or share the machine using Tailscale's sharing feature)
|
||||
3. **Find the server's Tailscale IP** -- shown in the Tailscale app, typically `100.x.y.z`
|
||||
4. **Disable TLS in config** -- set `tls.mode` to `"off"` in `config.yaml` since Tailscale already encrypts all traffic with WireGuard
|
||||
5. **Connect clients** using the Tailscale IP: `100.x.y.z:8443`
|
||||
|
||||
## Benefits
|
||||
|
||||
- No port forwarding needed
|
||||
- Works behind CGNAT and strict firewalls
|
||||
- Encrypted by default (WireGuard)
|
||||
- Stable IPs that don't change
|
||||
- Easy to add/remove friends via the Tailscale admin console
|
||||
Reference in New Issue
Block a user