WebRTC

TURN/STUN credentials for peer-to-peer video streaming

WebRTC API

Retrieve credentials for STUN/TURN servers to enable peer-to-peer video streaming.

Base URL:http://preview.cliqer.io | WebSocket:wss://preview.cliqer.io/ws

Get TURN Credentials

Retrieve temporary TURN server credentials for WebRTC connections.

Endpoint: GET /api/turn-credentials

Origin-gated, no session, This endpoint is protected by an Origin allow-list, not authentication. Browsers automatically send an allowed Origin; server-to-server callers must set one (e.g. Origin: https://cliqer.io). Requests from a disallowed or missing origin receive 403 Forbidden.
interface TurnCredentials {
  // Ready-to-use ICE servers: a Cloudflare STUN entry plus Cloudflare-minted
  // TURN entries (each with its own username/credential).
  iceServers: RTCIceServer[]
  ttl: number
}

const credentials: TurnCredentials = await $fetch('/api/turn-credentials')

Response:

{
  "iceServers": [
    { "urls": "stun:stun.cloudflare.com:3478" },
    {
      "urls": [
        "turn:turn.cloudflare.com:3478?transport=udp",
        "turn:turn.cloudflare.com:3478?transport=tcp",
        "turns:turn.cloudflare.com:5349?transport=tcp"
      ],
      "username": "<cloudflare-generated>",
      "credential": "<cloudflare-generated>"
    }
  ],
  "ttl": 86400
}

Using TURN Credentials

Browser (JavaScript)

const credentials = await $fetch('/api/turn-credentials')

// The response already contains ready-to-use ICE servers, pass them straight through.
const peerConnection = new RTCPeerConnection({
  iceServers: credentials.iceServers
})

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    ws.send(JSON.stringify({
      type: 'webrtcSignal',
      payload: {
        room: roomId,
        signal: { type: 'candidate', candidate: event.candidate }
      }
    }))
  }
}

WebRTC Signaling via WebSocket

Cliqer uses a perfect negotiation pattern for WebRTC signaling. The flow:

  1. Non-initiator sends ready signal to announce presence
  2. Initiator creates offer upon receiving ready
  3. Non-initiator responds with answer
  4. Both exchange ICE candidates

Signal Types

TypeSenderPurpose
readyNon-initiatorAnnounce ready to receive offer
offerInitiatorSDP offer for connection
answerNon-initiatorSDP answer to offer
candidateBothICE candidate for NAT traversal
// Non-initiator signals ready
ws.send(JSON.stringify({
  event: 'message',
  payload: {
    type: 'webrtcSignal',
    payload: {
      room: 'ABC123',
      signal: { type: 'ready' }
    }
  }
}))

// Initiator sends offer
ws.send(JSON.stringify({
  event: 'message',
  payload: {
    type: 'webrtcSignal',
    payload: {
      room: 'ABC123',
      signal: {
        type: 'offer',
        sdp: peerConnection.localDescription?.sdp
      }
    }
  }
}))

// Handle incoming signals
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  if (msg.type === 'webrtcSignal') {
    const { signal, from } = msg.payload
    
    switch (signal.type) {
      case 'ready':
        // Create and send offer (if initiator)
        break
      case 'offer':
        await peerConnection.setRemoteDescription(signal)
        const answer = await peerConnection.createAnswer()
        await peerConnection.setLocalDescription(answer)
        // Send answer back
        break
      case 'answer':
        await peerConnection.setRemoteDescription(signal)
        break
      case 'candidate':
        await peerConnection.addIceCandidate(signal.candidate)
        break
    }
  }
}

Server Configuration

ServerURLPortProtocolPurpose
Cloudflare STUNstun:stun.cloudflare.com3478UDPPublic IP discovery (always included)
Cloudflare TURNturn:turn.cloudflare.com3478UDP / TCPRelay, minted per request
Cloudflare TURNSturns:turn.cloudflare.com5349TLS/TCPSecure relay fallback
The STUN entry is always the fixed Cloudflare STUN URL. The TURN entries (URLs, username, credential) are returned dynamically by Cloudflare's TURN service, so the exact hostnames/ports may differ from those shown, always use the iceServers array from the response verbatim.

Credential Lifetime

  • TTL: 86400 seconds (24 hours)
  • Source: Credentials are minted on demand by Cloudflare's TURN REST API (rtc.live.cloudflare.com/v1/turn/keys/…/credentials/generate); each response carries its own short-lived username / credential.

Rate Limiting & Security

EndpointAuthRate limit
/api/turn-credentialsNone (Origin allow-list)Not rate-limited
This endpoint is not authenticated and not per-IP rate-limited, a presentation audience is often many attendees behind a single NAT IP, so a per-IP cap would break the whole room. Access is gated by an Origin allow-list instead; requests from a disallowed or missing origin return 403 Forbidden.