# Jitsi Meet

A self-hosted [Jitsi Meet](https://jitsi.org/jitsi-meet/) needs a TURN server for participants on networks that block UDP or use strict NATs. Without it, those users see frozen video, one-way audio, or cannot join.

There are two ways to connect Jitsi to Turnix:

| | Option A: shared secret (recommended) | Option B: static credentials |
|---|---|---|
| How | Prosody generates fresh TURN credentials per user from your [auth secret](https://turnix.io/docs/integrations/shared-secret) | You paste one credential from the dashboard into the config |
| Expiry | Handled automatically | The credential stops working at its TTL; you must replace it |
| Use for | Production | A quick test |

> **Tip: Let your AI agent configure it**
>
> Run Claude Code (or another agent with shell access) on your Jitsi server, set `TURNIX_AUTH_SECRET` in that shell, and paste the prompt below. The agent configures Prosody (Option A), checks the config and restarts the services.

**AI prompt: Configure Jitsi Meet with Turnix**

```text
Configure the self-hosted Jitsi Meet on this server to use Turnix (managed TURN/STUN) through Prosody's mod_external_services with a shared secret.

Context about Turnix:
- STUN server: stun.turnix.io port 3478
- TURN servers: <region>.turnix.io, port 3478 (turn, UDP and TCP) and port 443 (turns, TCP). Ask me which region to use (for example us-west) if I have not said so. The region list is at https://turnix.io/app/turn/server.
- The shared secret is my Turnix project's auth secret, available in the environment variable TURNIX_AUTH_SECRET. Never print it or include it in your summary.
- Docs: https://turnix.io/docs/integrations/jitsi

Do the following:
1. Find the Prosody config for the Jitsi domain (usually /etc/prosody/conf.avail/<domain>.cfg.lua) and make a timestamped backup copy. If TURNIX_AUTH_SECRET is empty, stop and ask me to set it.
2. Check that "external_services" is in modules_enabled of the main VirtualHost; add it if missing.
3. At the top level of the file, replace any existing external_service_secret and external_services settings with:
   external_service_secret = "<value of TURNIX_AUTH_SECRET>";
   external_services = {
     { type = "stun", host = "stun.turnix.io", port = 3478 },
     { type = "turn", host = "<region>.turnix.io", port = 3478, transport = "udp", secret = true, ttl = 86400, algorithm = "turn" },
     { type = "turn", host = "<region>.turnix.io", port = 3478, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" },
     { type = "turns", host = "<region>.turnix.io", port = 443, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
   };
   Show me the diff with the secret masked.
4. Run prosodyctl check config and fix any error you introduced. Then restart: systemctl restart prosody jicofo jitsi-videobridge2.
5. If a local coturn is installed and no longer referenced, tell me, but do not stop or remove it without asking.
6. Tell me how to verify: join a meeting from a network that blocks UDP and look for relay candidates on <region>.turnix.io in chrome://webrtc-internals, or check Sessions in the Turnix dashboard (https://turnix.io/app/turn/sessions).
```

## Before you start

- A working self-hosted Jitsi Meet (for example the Debian/Ubuntu packages from the [Jitsi handbook](https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-quickstart)), with shell access to the server
- A Turnix project (the free plan works)

## Option A: shared secret via Prosody (recommended)

Jitsi clients get their STUN/TURN servers from Prosody's `mod_external_services`. Point it at Turnix.

### 1. Get the Turnix values

1. Open [LTC/Auth Token](https://turnix.io/app/turn/ltc) in the dashboard and click **Create** (or copy the existing secret).
2. Open [Server](https://turnix.io/app/turn/server) and copy the endpoint of an online region close to your users, for example `us-west.turnix.io`.

### 2. Configure Prosody

Edit your Prosody config, usually `/etc/prosody/conf.avail/<your-domain>.cfg.lua`.

Make sure `external_services` is listed in `modules_enabled` of your main `VirtualHost` (the Debian packages enable it by default). Then replace any existing `external_service_secret` and `external_services` settings at the top level of the file with:

```lua
external_service_secret = "YOUR_TURNIX_AUTH_SECRET";
external_services = {
    { type = "stun", host = "stun.turnix.io", port = 3478 },
    { type = "turn", host = "us-west.turnix.io", port = 3478, transport = "udp", secret = true, ttl = 86400, algorithm = "turn" },
    { type = "turn", host = "us-west.turnix.io", port = 3478, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" },
    { type = "turns", host = "us-west.turnix.io", port = 443, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
};
```

`ttl` is how long each generated credential is valid, in seconds. Prosody generates a new one every time a participant joins.

If your installation still runs a local coturn that you no longer need, remove its entries from `external_services` and stop the service.

### 3. Restart and verify

```bash
sudo systemctl restart prosody jicofo jitsi-videobridge2
```

Join a meeting from a restrictive network (for example a corporate VPN or a guest Wi-Fi that blocks UDP). In `chrome://webrtc-internals` you should see `relay` candidates on `us-west.turnix.io`, and the session appears under [Sessions](https://turnix.io/app/turn/sessions) in the Turnix dashboard.

## Option B: static credentials (quick test)

1. In the Turnix dashboard, open [Credentials](https://turnix.io/app/turn/credentials) and create a new credential. Choose a region and a TTL (for a test, a few hours is enough).
2. In the result, switch to the **JitsiMeet** tab and copy the JSON array.
3. Paste it as the STUN/TURN server list in your Jitsi Meet `config.js` (usually `/etc/jitsi/meet/<your-domain>-config.js`), under `p2p.stunServers`:

```javascript
p2p: {
    enabled: true,
    stunServers: [
        { urls: "stun:stun.turnix.io:3478" },
        { urls: "turn:us-west.turnix.io:3478?transport=udp", username: "<username>", credential: "<credential>" },
        { urls: "turn:us-west.turnix.io:3478?transport=tcp", username: "<username>", credential: "<credential>" },
        { urls: "turns:us-west.turnix.io:443?transport=tcp", username: "<username>", credential: "<credential>" }
    ]
},
```

> **Warning: Static credentials expire**
>
> The credential stops working at the TTL you chose, and anyone who opens your meeting page can read it from `config.js`. Use Option A for production.

`p2p.stunServers` applies to one-to-one calls in peer-to-peer mode. Calls through the videobridge get their TURN servers from Prosody, so Option A covers both.

## Troubleshooting

| Problem | Fix |
|---------|-----|
| No `relay` candidates | Check that the secret matches the dashboard and that `mod_external_services` is enabled. Run `prosodyctl check config`. |
| Worked, then stopped | You rotated the auth secret (update Prosody), or the static credential from Option B expired. |
| Only users on restrictive networks fail | Make sure the `turns` entry on port `443` over TCP is present. |
