Nginx

Jspreadsheet Server listens on one port and speaks both HTTP (REST API) and WebSocket (real-time sync) on it. Nginx sits in front for TLS termination, domain routing and rate limiting. The WebSocket upgrade must be configured, because without the Upgrade/Connection headers the Socket.IO handshake falls back or fails.

Dedicated subdomain

In the recommended layout the server owns a whole hostname. This is the configuration used in the deployment guide:

Server block with WebSocket upgrade

server {
    listen 443 ssl;
    server_name sheets.example.com;

    ssl_certificate     /etc/letsencrypt/live/sheets.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sheets.example.com/privkey.pem;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;
    }
}

Read timeout for idle WebSockets

Set proxy_read_timeout explicitly: idle WebSocket connections are long-lived, and the default 60s would disconnect inactive collaborators.

Path prefix on an existing site

To mount the server under a path (e.g. https://example.com/sheets/) alongside an existing application:

Location block with prefix stripping

    location /sheets/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;
    }

The trailing slash on proxy_pass strips the /sheets prefix before forwarding.

Client configuration

Point the client at the prefixed URL:

const remote = client.connect({
    url: 'https://example.com/sheets',
    auth: { token },
});

Rate limiting

The server serializes operations per document, but auth-failure storms and anonymous Formify submissions are best throttled at the proxy:

Throttle zone for the API

limit_req_zone $binary_remote_addr zone=sheets_api:10m rate=30r/s;

    location /api/ {
        limit_req zone=sheets_api burst=60 nodelay;
        proxy_pass http://127.0.0.1:3000;
        # ... same proxy headers as above
    }

Notes

Use TLS in production

Tokens travel in the WebSocket handshake query string and in Bearer headers. See the security checklist.

Configure CORS on the server

Set the allowed origins in the server's config.cors option rather than adding Access-Control-Allow-Origin headers in Nginx, because setting both produces invalid duplicate headers.

Multiple instances

Do not round-robin. One server process owns each live document, so upstream balancing must route by document guid. The topology is explained in Scaling.

What's Next?

  • Deployment: the full production setup this proxy fronts.
  • Scaling: guid routing across multiple instances.