This example pairs a werift TypeScript signaling server with a Vite + React SPA client.
The server:
- accepts
POST /sessionover HTTPS, - creates a werift
RTCPeerConnectionoffer withcreateDataChannel("loopback"), - returns the offer plus a
turns:URL and one-time TURN credentials, - accepts
PUT /sessionwith{ username, answer }, - multiplexes HTTPS signaling and TURN over TLS on the same public port,
- echoes every received DataChannel message back to the client.
- serves the built SPA itself so a single HTTPS address can host both the UI and TURN/TLS signaling.
The client:
- fetches the server offer and TURN credentials,
- creates a browser
RTCPeerConnectionwithiceTransportPolicy: "relay", - waits for ICE gathering to complete before the
PUT /session, - receives the server-created DataChannel with
ondatachannel, - sends one echo message when the channel opens and shows the loopback result.
- can target any signaling/TURN server URL in dev mode, while production builds are pinned to the hosting origin.
cd examples/turn-loopback
npm install
npm run serverIn another terminal:
cd examples/turn-loopback
npm run clientThen open the Vite URL, enter any HTTPS signaling base URL that serves this example's /session API, and click Start echo session.
cd examples/turn-loopback
npm install
npm run build
npm run serverOpen https://127.0.0.1:8443/ and the server-hosted SPA will use the same origin for both HTTPS signaling and the returned turns: URI.
Build from the repository root so the example can access the workspace packages it imports directly:
cd examples/turn-loopback
npm run docker:buildThe image now starts two processes inside one container:
- werift server on
8443for HTTPSPOST/PUT /session, SPA hosting, and TURN/TLS multiplexing. - nginx on
8080for/.well-known/acme-challenge/*, plainGET /health, and HTTP → HTTPS redirect.
Run it with the public port mapping expected by browsers and ACME:
docker run --rm \
-p 80:8080 \
-p 443:8443 \
-e TURN_LOOPBACK_PUBLIC_HOST=localhost \
werift-turn-loopbackWhen CERTBOT_DOMAINS is unset, the entrypoint generates a short-lived self-signed certificate inside the container and points TURN_LOOPBACK_CERT_FILE / TURN_LOOPBACK_KEY_FILE at it automatically. Open https://localhost/ (or https://127.0.0.1/) and trust or bypass the local certificate warning as needed.
To request an ACME certificate at startup instead, publish the container on public ports 80/443 and set the Certbot variables:
docker run --rm \
-p 80:8080 \
-p 443:8443 \
-e TURN_LOOPBACK_PUBLIC_HOST=example.com \
-e CERTBOT_DOMAINS=example.com \
-e CERTBOT_EMAIL=ops@example.com \
-e CERTBOT_STAGING=1 \
werift-turn-loopbackIn this mode nginx serves /.well-known/acme-challenge/* from the configured webroot over HTTP, redirects every other HTTP request to HTTPS, and Certbot writes the resulting certificate material to container-local paths before the werift server starts.
cd examples/turn-loopback
npm run chrome-e2eThe harness builds the SPA, starts the HTTPS/TURN server plus the Vite dev server, and verifies:
- the single-address hosted SPA can complete echo,
- multiple pages can connect concurrently with independent sessions,
- the dev SPA can point at an arbitrary server URL,
- Chromium runs with
--ignore-certificate-errorsand--allow-insecure-localhost.
| Variable | Default | Purpose |
|---|---|---|
TURN_LOOPBACK_HOST |
0.0.0.0 |
Bind address for the shared TLS listener |
TURN_LOOPBACK_HTTP_PORT |
8080 in Docker |
Bind address for the plain HTTP challenge / redirect app |
TURN_LOOPBACK_PORT |
8443 |
Shared public port for HTTPS + TURN/TLS |
TURN_LOOPBACK_PUBLIC_AUTHORITY |
unset | Explicit host:port authority used in the returned turns: URL |
TURN_LOOPBACK_PUBLIC_HOST |
request Host header, else 127.0.0.1 |
Hostname used when building the returned turns: URL |
TURN_LOOPBACK_PUBLIC_PORT |
same as TURN_LOOPBACK_PORT (443 in Docker image) |
External port used when the request Host header omits a port |
TURN_LOOPBACK_RELAY_ADDRESS |
127.0.0.1 |
Relay address advertised by the TURN server |
TURN_LOOPBACK_RELAY_BIND_ADDRESS |
same as host | Bind address for relay UDP sockets |
TURN_LOOPBACK_REALM |
turn-loopback.local |
TURN realm |
TURN_LOOPBACK_PENDING_SESSION_TTL_MS |
60000 |
TTL before an unanswered session is cleaned up |
TURN_LOOPBACK_ACTIVE_SESSION_TTL_MS |
300000 |
TTL for established sessions |
TURN_LOOPBACK_CERT_PEM |
bundled self-signed cert | PEM body for the TLS certificate |
TURN_LOOPBACK_KEY_PEM |
bundled self-signed key | PEM body for the TLS private key |
TURN_LOOPBACK_CERT_FILE |
none | Path to a TLS certificate PEM file |
TURN_LOOPBACK_KEY_FILE |
none | Path to a TLS private key PEM file |
CERTBOT_DOMAINS |
unset | Comma- or space-separated domain list to request with certbot certonly --webroot |
CERTBOT_EMAIL |
unset | Registration email for Certbot; omitted uses --register-unsafely-without-email |
CERTBOT_STAGING |
0 |
Set to 1 / true / yes to use the ACME staging environment |
CERTBOT_WEBROOT |
/var/www/turn-loopback in Docker |
Webroot served by the HTTP app for /.well-known/acme-challenge/* |
CERTBOT_STATE_DIR |
/var/lib/turn-loopback/certbot in Docker |
Container-local Certbot config / work / logs directory |
CERTBOT_RENEW_INTERVAL |
0 in Docker |
Seconds between certbot renew attempts; 0 disables the background renew loop |
VITE_SIGNALING_BASE_URL |
https://127.0.0.1:8443 |
Default dev-mode server URL shown in the SPA |
- The werift server peer does not set
iceTransportPolicy: "relay"and does not configure TURN client settings. - The browser client does force relay-only ICE and uses the server-provided
turns:URI. - Production builds always call the same origin that hosted the SPA; the editable signaling URL field is only for dev mode.
- Browsers are strict about
turns:certificates. For local container use, trust the generated self-signed certificate first or provide publicly trusted / locally trusted certificate material. - Each
POST /sessioncreates unique TURN credentials, so multiple tabs or devices can run independent echo sessions concurrently. - The Docker image keeps HTTP challenge state and issued certificates inside the container only. Without a persistent volume, a restart repeats setup and may request a fresh certificate.
- Repeated non-persistent startup against the ACME production endpoint can hit rate limits. Use
CERTBOT_STAGING=1until DNS, port publishing, and redirect behavior are confirmed. CERTBOT_RENEW_INTERVALonly refreshes files on disk. The running werift process does not hot-reload TLS material, so long-lived containers still need a restart to begin serving a renewed certificate.- If your external HTTPS/TURN authority differs from the container bind port mapping, set
TURN_LOOPBACK_PUBLIC_AUTHORITYorTURN_LOOPBACK_PUBLIC_HOST+TURN_LOOPBACK_PUBLIC_PORTso the returnedturns:URI stays aligned with the public443endpoint.