# Self-hosted TURN (coturn) on your server

Cross-Wi‑Fi calls need a TURN relay. This runs **coturn on your own machine**
(no Metered / Twilio). The app already reads `NEXT_PUBLIC_TURN_*` from
`runtime-config.js` via `deploy.sh`.

## 1. Firewall / AWS security group

Open inbound on the TURN host:

| Proto | Ports | Why |
|-------|--------|-----|
| UDP | 3478 | TURN |
| TCP | 3478 | TURN (UDP blocked on some networks) |
| UDP | 49160–49200 | Media relay (matches `turnserver.conf`) |

Outbound: allow all (default).

## 2. Configure coturn

On the server (example path matches your deploy tree):

```bash
cd /var/www/html/connect/ops/coturn

# Edit turnserver.conf:
#   - user=connecthub:YOUR_LONG_SECRET
#   - external-ip=YOUR.PUBLIC.IP   (curl -s https://checkip.amazonaws.com)
#   - realm=api-new.windshieldhub.com  (or your domain)

docker compose up -d
docker compose logs -f   # should show "turn server ready"
```

If Docker is not installed: `sudo apt install docker.io docker-compose-v2` (Ubuntu).

## 3. Point the app at your TURN

In `/var/www/html/connect/.env` (app root, not this folder):

```bash
# Use the EC2 public IP — NOT api-new.windshieldhub.com if that name is
# behind Cloudflare (orange cloud). CF does not proxy TURN/UDP 3478.
NEXT_PUBLIC_TURN_URLS=turn:34.221.99.35:3478,turn:34.221.99.35:3478?transport=tcp
NEXT_PUBLIC_TURN_USERNAME=connecthub
NEXT_PUBLIC_TURN_CREDENTIAL=YOUR_LONG_SECRET
```

Use the **same** username/password as `user=` in `turnserver.conf`.

Better long-term: create a DNS-only (grey cloud) record, e.g.
`turn.windshieldhub.com → 34.221.99.35`, then:

```bash
NEXT_PUBLIC_TURN_URLS=turn:turn.windshieldhub.com:3478,turn:turn.windshieldhub.com:3478?transport=tcp
```

Redeploy so `runtime-config.js` picks it up:

```bash
cd /var/www/html/connect && ./deploy.sh
```

Hard-refresh both browsers, then call across two different Wi‑Fi networks.

## 4. Verify

```bash
# Process listening
sudo ss -ulnp | grep 3478

# From your laptop (optional): install turnutils
# turnutils_uclient -v -u connecthub -w YOUR_LONG_SECRET YOUR.PUBLIC.IP
```

In the browser during a cross-Wi‑Fi call, ICE candidates should include
`typ relay` (not only `typ host`).

## 5. "TURN credentials" / still Connecting

That UI message means the **app sees TURN config**, but WebRTC still failed.

Check in order:

1. **Browser console** during a call — look for  
   `[webrtc] ICE gathering complete { sawRelay: true/false, ... }`  
   - `sawRelay: false` → coturn allocate failed (password, firewall, CSP, Cloudflare hostname, or coturn down).  
   - `sawRelay: true` → relay OK; open UDP **49160–49200** on the host.

2. **Hostname must bypass Cloudflare.**  
   `api-new.windshieldhub.com` resolving to `104.26.x` / `104.21.x` means TURN
   never hits your EC2. Point `NEXT_PUBLIC_TURN_URLS` at the EC2 IP or a
   grey-cloud DNS name.

3. **`runtime-config.js`** on production must include all three:
   ```js
   NEXT_PUBLIC_TURN_URLS: "turn:api-new.windshieldhub.com:3478,...",
   NEXT_PUBLIC_TURN_USERNAME: "connecthub",
   NEXT_PUBLIC_TURN_CREDENTIAL: "same-as-coturn-user-line"
   ```
   Username/password alone missing is a common miss.

3. **coturn `external-ip`** must be the real public IPv4 (not `YOUR.PUBLIC.IP.HERE`).  
   On AWS NAT, prefer: `external-ip=<public>/<private>`.

4. **Security group**: UDP/TCP 3478 + UDP 49160–49200.

5. **Desktop app**: rebuild after CSP update (`stun: turn: turns:` in `connect-src`).  
   Old installers block TURN.

6. coturn logs: `docker compose logs -f` — look for `401` (bad password) or bind errors.
