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):
- Hocuspocus Yjs Collaboration: Upgrades on
/api/v1/editor/collab. - 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
}
}Domain & Cookie Configuration (COOKIE_DOMAIN)
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_DOMAINunset (or commented out) inapi/.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_DOMAINto the leading-dot root domain inapi/.env:COOKIE_DOMAIN=".example.com" - Requirements:
- HTTPS is mandatory when
COOKIE_DOMAINis set (Better Auth automatically appliesSameSite=None; Secure). CORS_ORIGINSinapi/.envmust include your web origin:CORS_ORIGINS="https://cms.example.com"
- HTTPS is mandatory when
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=1This ensures:
- Rate-limiting middleware calculates limits per client IP rather than throttling your reverse proxyβs internal loopback IP (
127.0.0.1). - 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.