Skip to Content
πŸ–₯️ Self-HostingReverse Proxy & Production

Reverse Proxy & Production

In a production environment, Sitepins runs behind a reverse proxy (such as Nginx or Caddy) to handle SSL/TLS termination, static asset compression, request routing, and WebSocket upgrades.

The WebSocket Requirement

Sitepins relies on two independent real-time WebSocket channels hosted on the Express API process (port 4000):

  1. Hocuspocus Yjs Collaboration: Upgrades on /api/v1/editor/collab.
  2. Socket.IO Real-time Presence: Upgrades on /socket.io/.

Crucial Reverse Proxy Configuration: If your reverse proxy does not explicitly pass Upgrade and Connection headers for WebSocket paths, real-time collaboration, cursor sync, and live commit notifications will drop or fail with connection timeouts.


Nginx Configuration

Below is a complete, production-ready Nginx virtual host configuration. It routes the web interface to port 3000, the API to port 4000, and properly forwards WebSockets:

# Upstream definitions upstream sitepins_web { server 127.0.0.1:3000; keepalive 32; } upstream sitepins_api { server 127.0.0.1:4000; keepalive 32; } # WebSocket upgrade map map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name cms.example.com api.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name cms.example.com; ssl_certificate /etc/letsencrypt/live/cms.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/cms.example.com/privkey.pem; client_max_body_size 25M; # Gzip compression gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; # Next.js Web App location / { proxy_pass http://sitepins_web; proxy_http_version 1.1; 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; # WebSockets for Next.js Fast Refresh proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; client_max_body_size 25M; # 1. Standard Express REST Routes location / { proxy_pass http://sitepins_api; proxy_http_version 1.1; 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; } # 2. Hocuspocus Yjs Collaboration WebSocket location /api/v1/editor/collab { proxy_pass http://sitepins_api; proxy_http_version 1.1; 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_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # 3. Socket.IO Presence and Events location /socket.io/ { proxy_pass http://sitepins_api; proxy_http_version 1.1; 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_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } }

Caddy Configuration

Caddy automatically provisions SSL certificates via Let’s Encrypt and supports WebSocket upgrading natively:

cms.example.com { reverse_proxy 127.0.0.1:3000 } api.example.com { reverse_proxy 127.0.0.1:4000 }

If hosting the web app and API on a single domain using path-based routing:

cms.example.com { # Route API and real-time paths to Express handle /api/v1/* { reverse_proxy 127.0.0.1:4000 } handle /socket.io/* { reverse_proxy 127.0.0.1:4000 } handle /healthz { reverse_proxy 127.0.0.1:4000 } # Route all other traffic to Next.js handle { reverse_proxy 127.0.0.1:3000 } }

Session authentication uses HTTP-only cookies signed by Better Auth. How you configure domains dictates your cookie settings:

Scenario A: Same-Domain or Subpath Setup

  • Web App: https://cms.example.com
  • API Server: https://cms.example.com/api/v1
  • Configuration: Leave COOKIE_DOMAIN unset (or commented out) in api/.env.
  • Standard first-party cookies are used.

Scenario B: Multi-Subdomain Setup

  • Web App: https://cms.example.com
  • API Server: https://api.example.com
  • Configuration: Set COOKIE_DOMAIN to the leading-dot root domain in api/.env:
    COOKIE_DOMAIN=".example.com"
  • Requirements:
    1. HTTPS is mandatory when COOKIE_DOMAIN is set (Better Auth automatically applies SameSite=None; Secure).
    2. CORS_ORIGINS in api/.env must include your web origin:
      CORS_ORIGINS="https://cms.example.com"

Reverse Proxy IP Forwarding (TRUST_PROXY)

When running behind Nginx or Caddy, client IP addresses are forwarded in the X-Forwarded-For header.

In api/.env, ensure:

TRUST_PROXY=1

This ensures:

  1. Rate-limiting middleware calculates limits per client IP rather than throttling your reverse proxy’s internal loopback IP (127.0.0.1).
  2. Audit logs record the actual browser client IP address.

Architecture Note: Single-Instance Scaling

Both Socket.IO and Hocuspocus maintain room rosters and collaborative CRDT states in process memory.

Run a single API instance. Do not run multiple backend API processes behind a round-robin load balancer without a shared Redis adapter. A user connected to Instance A will not see presence or cursor updates from a collaborator connected to Instance B. A single API instance easily handles hundreds of concurrent collaborative editing sessions.

Last updated on