# MatrixRTC / LiveKit — Element X Calls

**Last updated:** 2026-03-19

MatrixRTC enables voice/video calls in Element X using a LiveKit SFU backend. Both homeservers (`mx.vish.gg` and `matrix.thevish.io`) share the same LiveKit SFU on `matrix-ubuntu`.

---

## Service Overview

| Property | Value |
|----------|-------|
| **Host** | matrix-ubuntu (`192.168.0.154`) |
| **Matrix homeservers** | `mx.vish.gg` (synapse-mx.service) and `matrix.thevish.io` (synapse.service) — both on Synapse 1.148.0 |
| **Compose file** | `hosts/vms/matrix-ubuntu/livekit.yml` (deployed manually at `/opt/livekit/`) |
| **LiveKit version** | 1.9.12 |
| **JWT service** | `ghcr.io/element-hq/lk-jwt-service:latest-ci` |

---

## Architecture

```
Element X  ──→  mx.vish.gg (.well-known)         ──→  livekit.mx.vish.gg/livekit/jwt  (JWT service)
           ──→  matrix.thevish.io (.well-known)  ──→  livekit.mx.vish.gg/livekit/sfu  (LiveKit SFU)
                                                       livekit.mx.vish.gg/             (LiveKit WS)
```

Both homeservers share the same LiveKit backend.

- **NPM** on Calypso proxies `livekit.mx.vish.gg` → matrix-ubuntu
  - `/livekit/jwt/` → JWT service port 8089 (container 8080)
  - `/livekit/sfu/` → LiveKit SFU port 7880
  - `/` → LiveKit SFU port 7880 (WebSocket for direct connections)
- **DNS**: `livekit.mx.vish.gg` A record unproxied → `184.23.52.14` (home WAN)
- **TLS**: Let's Encrypt cert issued via Cloudflare DNS challenge on matrix-ubuntu, copied to NPM as `npm-7`

---

## Endpoints

| Endpoint | URL | Purpose |
|----------|-----|---------|
| JWT service healthz | `https://livekit.mx.vish.gg/livekit/jwt/healthz` | Health check |
| JWT service SFU get | `https://livekit.mx.vish.gg/livekit/jwt/sfu/get` | Token exchange |
| LiveKit SFU WS | `wss://livekit.mx.vish.gg/livekit/sfu/` | WebSocket signalling |
| LiveKit HTTP | `https://livekit.mx.vish.gg/` | SFU API |
| .well-known | `https://mx.vish.gg/.well-known/matrix/client` | RTC foci advertisement |

---

## Configuration Files on matrix-ubuntu

| File | Purpose |
|------|---------|
| `/opt/livekit/docker-compose.yml` | LiveKit + JWT service deployment |
| `/opt/livekit/livekit.yaml` | LiveKit SFU config (keys, RTC ports, external IP) |
| `/opt/synapse-mx/homeserver.yaml` | Synapse config (MSCs, rate limits) |
| `/etc/nginx/sites-available/mx-vish-gg` | nginx serving `.well-known` and Element static files |
| `/etc/letsencrypt/live/livekit.mx.vish.gg/` | TLS cert (auto-renews, copies to NPM via deploy hook) |

---

## Synapse homeserver.yaml additions

```yaml
# MatrixRTC / Element Call support
experimental_features:
  msc3266_enabled: true   # Room Summary API (knocking over federation)
  msc4222_enabled: true   # state_after in sync v2
  msc4140_enabled: true   # Delayed events (call participation signalling)

max_event_delay_duration: 24h

rc_message:
  per_second: 0.5
  burst_count: 30

rc_delayed_event_mgmt:
  per_second: 1
  burst_count: 20
```

---

## .well-known/matrix/client

Served by nginx at `https://mx.vish.gg/.well-known/matrix/client`:

```json
{
  "m.homeserver": {"base_url": "https://mx.vish.gg"},
  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://livekit.mx.vish.gg/livekit/jwt"
    }
  ]
}
```

---

## LiveKit SFU config (/opt/livekit/livekit.yaml)

Key settings:
- `use_external_ip: true` — auto-detects WAN IP `184.23.52.14`
- `use_ice_lite: true` — optimised for server-side NAT traversal
- `room.auto_create: false` — only lk-jwt-service creates rooms (security)
- RTC ports: 7880 TCP (API/WS), 7881 TCP (RTC), 50000-60000 UDP (media)
- **Note:** UDP 50000-60000 port range is NOT currently forwarded on the router — TURN relay is used instead via coturn at `turn:mx.vish.gg:3479`

---

## TLS Certificate Renewal

Cert is issued on matrix-ubuntu via certbot + Cloudflare DNS plugin. A deploy hook copies it to NPM on Calypso after renewal:

```
/etc/letsencrypt/renewal-hooks/deploy/copy-to-npm.sh
```

If the hook fails, manually copy:
```bash
ssh matrix-ubuntu
sudo cp /etc/letsencrypt/live/livekit.mx.vish.gg/fullchain.pem \
  /tmp/lk.crt
sudo cp /etc/letsencrypt/live/livekit.mx.vish.gg/privkey.pem \
  /tmp/lk.key
scp -P 62000 /tmp/lk.crt Vish@100.103.48.78:/volume1/docker/nginx-proxy-manager/data/custom_ssl/npm-7/fullchain.pem
scp -P 62000 /tmp/lk.key Vish@100.103.48.78:/volume1/docker/nginx-proxy-manager/data/custom_ssl/npm-7/privkey.pem
ssh -p 62000 Vish@100.103.48.78 'sudo /usr/local/bin/docker exec nginx-proxy-manager nginx -s reload'
```

---

## Troubleshooting

### Calls not working in Element X
1. Check `.well-known` is advertising foci: `curl https://mx.vish.gg/.well-known/matrix/client`
2. Check JWT service: `curl https://livekit.mx.vish.gg/livekit/jwt/healthz`
3. Check LiveKit is running: `ssh matrix-ubuntu "sudo docker ps | grep livekit"`
4. Check LiveKit logs: `ssh matrix-ubuntu "sudo docker logs livekit 2>&1 | tail -20"`

### Stuck calls
See [How to resolve stuck MatrixRTC calls](https://sspaeth.de/2025/02/how-to-resolve-stuck-matrixrtc-calls/) — usually caused by delayed events not cleaning up.

### JWT service returns 400
Normal for unauthenticated requests. Means the service is running correctly.

### Restarting services
```bash
ssh matrix-ubuntu
cd /opt/livekit
sudo docker compose restart
```

### Restarting Synapse
```bash
ssh matrix-ubuntu
sudo systemctl restart synapse-mx.service
```
