WebSocket Proxy Setup#

When using CloudXR.js with HTTPS hosting (for development or production), you need a WebSocket proxy with TLS support to establish secure connections from the browser to the CloudXR Runtime.

Overview#

A WebSocket proxy is required when:

  • Hosting your web application using HTTPS

  • Deploying to production environments with SSL certificates

  • Accessing CloudXR from remote networks or the internet

  • Using Pico 4 Ultra devices (which require HTTPS)

The proxy acts as a secure gateway, providing TLS termination for WebSocket connections.

Connection Architecture#

Without proxy (HTTP mode):

Browser →
ws://<server>:49100
CloudXR Runtime

With proxy (HTTPS mode):

Browser →
wss://<proxy>:48322 →
CloudXR Runtime (ws://localhost:49100)

Important

When hosting your web application using HTTPS, you must configure a WebSocket proxy and connect using wss://. Browsers block non-secure WebSocket (ws://) connections from secure (HTTPS) pages due to mixed content security policies.

Available Proxy Examples#

CloudXR.js supports the following common deployment scenarios:

Deployment Scenario

Example Solution

Setup Complexity

Local development with HTTP

No proxy needed (direct ws:// connection)

None

Development and testing with HTTPS

Docker + HAProxy example

Low

Single-server production

Docker + HAProxy example

Low

Kubernetes production

nginx Ingress example

Medium

Example 1: Development Proxy (Docker + HAProxy)#

This example uses HAProxy in a Docker container for HTTPS development and single-server deployments.

Quick Start#

  1. Build the proxy image:

    cd proxy
    docker build -t cloudxr-wss-proxy .
    
  2. Run the proxy:

    docker run -d --name wss-proxy \
      --network host \
      -e BACKEND_HOST=localhost \
      -e BACKEND_PORT=49100 \
      -e PROXY_PORT=48322 \
      cloudxr-wss-proxy
    
  3. View logs:

    docker logs -f wss-proxy
    

Configuration Options#

Variable

Default

Description

BACKEND_HOST

localhost

CloudXR Runtime hostname or IP.

BACKEND_PORT

49100

CloudXR Runtime WebSocket signaling port.

PROXY_PORT

48322

TLS proxy listening port.

CERT_HOSTNAMES

(empty)

Comma-separated extra hostnames or IPs for the auto-generated certificate’s subjectAltName. Required whenever clients reach the proxy by any name other than localhost, 127.0.0.1, or ::1 (e.g. a LAN IP from a phone or headset). Example: 192.168.1.42,xr-host.local. Modern browsers reject certificates without a matching SAN entry, so omitting this for non-localhost access produces ERR_CERT_COMMON_NAME_INVALID on the client.

HEALTH_CHECK_INTERVAL

2s

Time between backend health checks.

HEALTH_CHECK_RISE

2

Consecutive successful checks to mark backend up.

HEALTH_CHECK_FALL

3

Consecutive failed checks to mark backend down.

Note

The certificate is generated once on first run and cached on disk. To pick up a change to CERT_HOSTNAMES, either delete the existing server.pem (or wipe the volume if you persisted certificates) before restarting the container.

Using Custom Certificates#

If you have your own TLS certificate, combine certificate and key into a PEM and mount it:

cat your-cert.crt your-key.key > server.pem
docker run -d --name wss-proxy \
  --network host \
  -v /path/to/server.pem:/usr/local/etc/haproxy/certs/server.pem:ro \
  -e BACKEND_HOST=localhost \
  -e BACKEND_PORT=49100 \
  -e PROXY_PORT=48322 \
  cloudxr-wss-proxy

Managing the Proxy#

docker stop wss-proxy
docker start wss-proxy
docker stop wss-proxy
docker rm wss-proxy
docker stop wss-proxy
docker rm wss-proxy
docker volume rm cloudxr-proxy-certs

Note

To persist self-signed certificates across container restarts, mount a Docker volume: -v cloudxr-proxy-certs:/usr/local/etc/haproxy/certs.

Common Issues#

  • Connection refused at startup: Expected while CloudXR Runtime is still initializing.

  • Certificate warnings: Trust the proxy certificate on the headset or browser before connecting.

  • Firewall blocking: Ensure proxy TCP port (default 48322) is open.

  • SSL handshake failure (``certificate unknown``) from a LAN client: CERT_HOSTNAMES is missing the IP or hostname the client is using. Set it, remove the existing server.pem, and restart.

Browser Cross-Origin Access (Private Network Access)#

Modern Chromium-based browsers restrict requests from a public origin (for example a page served from https://example.github.io) to a private IP (for example https://10.x.x.x:48322) under the Private Network Access policy.

The proxy includes the server-side opt-in on every response:

Access-Control-Allow-Private-Network: true

This satisfies the preflight requirement. However, Chrome may additionally require a per-site user permission for the calling page. If a phone or desktop browser reports “Permission was denied for this request to access the local address space,” grant local-network permission on the calling origin (the page making the request), not on the proxy URL.

If you cannot or do not want to grant the permission, the simplest workaround is to host the WebXR application itself on the same private network as the proxy (private-to-private requests are not subject to the policy).

Example 2: Production Proxy (Kubernetes + nginx)#

For Kubernetes deployments, use nginx Ingress with TLS termination and WebSocket routing.

Typical setup flow:

  1. Create TLS secret.

  2. Deploy nginx proxy service and configuration.

  3. Configure Ingress for HTTPS + WebSocket traffic.

  4. Validate endpoint, then connect clients using wss://.

For CloudXR.js path-based proxy routing behavior, refer to Session API.

Firewall Configuration#

Refer to Ports and Firewalls for required ports and firewall configuration instructions.