# Self-hosting a CVB voice server

CVB is federated: anyone can run their own **voice server** (signaling, chat,
voice/screen SFU, music bot) and hand out their address to their friends. The
CVB master (`cvb.doinksndubstep.com`) is an **update hub only** - it never
participates in your voice traffic, stores none of your data, and your server
keeps working even when it is unreachable.

Who talks to whom:

```
master hub  <-- polls for releases, opt-in Apply -->  your voice server  <-- ws/wss + UDP -->  clients
```

Desktop clients update themselves **directly from the master** using a
signature-pinned manifest baked into the app - a voice server (even a
compromised one) cannot spoof a client update. Your server only tells clients
which client version is current so they see the update banner.

## Quickstart (Linux, Docker) - ~5 minutes

```bash
curl -o docker-compose.yml https://cvb.doinksndubstep.com/hub/install/docker-compose.yml
CVB_ANNOUNCED_ADDRESS=voice.example.com docker compose up -d
docker compose logs cvb-server | grep -A7 "FIRST BOOT"    # admin password
```

That is it. Requirements:

- **`CVB_ANNOUNCED_ADDRESS`**: the public hostname/IP clients use to reach
  you. Voice goes silent if this is wrong - it is what the media server
  advertises in ICE candidates.
- **Open in your cloud firewall**: TCP 4443 (WebSocket) and UDP 10000-10499
  (voice/screen). The compose file maps exactly these.
- **TLS (optional but nice)**: clients default to `wss://` for addresses
  without a port, and `ws://` when you give them `host:4443`. Either front
  4443 with your own Caddy/nginx (proxy to the container's 4443), or tell
  users to connect with the explicit port.
- **Admin panel**: published on the host's loopback only. Reach it from your
  machine with an SSH tunnel, then log in with the generated password:

  ```bash
  ssh -L 7443:127.0.0.1:7443 you@your.host
  # then open http://127.0.0.1:7443/
  ```

The first boot generates everything for you (nothing is committed, nothing is
shared): a random admin password (also written to
`cvb-data/admin-config/FIRST-RUN-CREDENTIALS.txt` - save it, then delete the
file) and a server id. The SQLite database seeds itself with the default
channels.

Generate invite codes for your friends from the admin panel (Invites ->
Generate). They install the CVB client from
<https://cvb.doinksndubstep.com>, enter your address (`voice.example.com:4443`
or just `voice.example.com` behind TLS), and register with the invite code.

## Quickstart (Windows)

From an elevated PowerShell:

```powershell
irm -OutFile install.ps1 https://cvb.doinksndubstep.com/hub/install/install-windows.ps1
.\install.ps1 -AnnouncedAddress voice.example.com
```

Checks Node 22+/ffmpeg/yt-dlp (with winget hints), installs under
`C:\cvb-server` by default, generates credentials, registers a boot-time
scheduled task, and opens the firewall for TCP 4443 + UDP 10000-10999.

## Updating your server (always opt-in)

Your server polls the master hub every 30 minutes. When a new release is out,
the admin panel shows it under **Server Updates** with the release notes.
Nothing changes until you type `UPDATE` and click **Apply**.

Applying:

1. Downloads the release tarball from the hub and verifies its sha256.
2. Extracts and `npm ci`s it into a fresh `versions/<v>/` tree (your running
   server is untouched during all of this).
3. Restarts CVB into the new tree. Users reconnect automatically; data,
   accounts, and invites are untouched.

**Roll back** instantly switches to the previous version and restarts. The
last two versions are kept on disk; older ones are pruned.

The container image itself only needs a pull (`docker compose pull && docker
compose up -d`) when base dependencies change; normal releases never require
it - the hub updater handles them in place.

## Persistence (back these up)

- `data/cvb.db` - users, messages (E2E ciphertext), invites, pets, coins
- `admin/config/` - admin password hash, session secret, server id
- Everything else is code and regenerable.

## Configuration reference

| Env var | Default | Purpose |
| --- | --- | --- |
| `CVB_ANNOUNCED_ADDRESS` | auto-detect | Public IP/hostname for ICE. **Set this.** |
| `CVB_HUB_URL` | `https://cvb.doinksndubstep.com` | Update hub. Empty string disables polling (air-gapped). |
| `CVB_HUB_TOKEN` | (none) | Only if the hub operator issued you a token. |
| `CVB_PORT` | `4443` | HTTP/WebSocket listen port. |
| `CVB_RTC_MIN_PORT` / `CVB_RTC_MAX_PORT` | `10000` / `10999` | Media UDP range (compose uses 10000-10499). |
| `CVB_HTTP_BIND_IP` | `0.0.0.0` | Bind interface. |
| `CVB_ADMIN_BIND` | `127.0.0.1` (Docker: `0.0.0.0` inside the container) | Admin panel bind. |
| `CVB_ADMIN_AUTOSTART` | `1` | Admin auto-starts the CVB server on boot. |
| `CVB_YTDLP_PATH` / `CVB_FFMPEG_PATH` | PATH lookup | Music bot binaries. |
| `CVB_SERVER_ROLE` | `voice` | `master` reveals the git-based Build & Deploy card (release host only). |

## Security model

- The admin panel is password-protected (argon2id) with login rate limiting
  and is never meant to be publicly reachable - loopback + SSH tunnel.
- Voice/chat traffic never touches the master. Client updates are
  minisign-verified against a key pinned in the client binary; they download
  from the master only, whatever server they are connected to.
- Server-code updates are sha256-verified against the hub manifest and land
  in isolated version trees, so a failed update can never half-apply.

## Troubleshooting

- **Voice silent across users**: `CVB_ANNOUNCED_ADDRESS` is wrong or the UDP
  range is firewalled. Check the startup log line `Mediasoup announces:`.
- **Clients can connect but no update banner**: expected until the master has
  a newer release than your server code; detection is per-server.
- **Admin login loops over the tunnel**: use a current browser; the session
  cookie is `Secure` and needs localhost's secure-context treatment.
