# Reverse Proxy Configuration

Pulse uses WebSockets for real-time updates. Your reverse proxy **MUST** support WebSocket connections or Pulse will not work correctly.

## Important Requirements

1. **WebSocket Support Required** - Enable WebSocket proxying
2. **Proxy Headers** - Forward original host and IP headers
3. **Timeouts** - Increase timeouts for long-lived connections (7 days recommended)
4. **Buffer Sizes** - Increase for large state updates (64KB recommended)
5. **Rate Limit Headers (v4.24.0+)** - Pass through `X-RateLimit-*` and `Retry-After` headers
6. **Error Pass-through** - Don't intercept 429 responses (rate limits)

## Authentication with Reverse Proxy

If you're using authentication at the reverse proxy level (Authentik, Authelia, etc.), you can disable Pulse's built-in authentication to avoid double login prompts:

```bash
# In your .env file or environment
DISABLE_AUTH=true
```

When `DISABLE_AUTH=true` is set:
- Pulse's built-in authentication is completely bypassed
- All endpoints become accessible without authentication
- The reverse proxy handles all authentication and authorization
- A warning is logged on startup to confirm auth is disabled

⚠️ **Warning**: Only use `DISABLE_AUTH=true` if your reverse proxy provides authentication. Never expose Pulse directly to the internet with authentication disabled.

## Nginx

```nginx
server {
    listen 80;
    server_name pulse.example.com;
    
    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name pulse.example.com;

    # SSL configuration
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # Proxy settings
    location / {
        proxy_pass http://localhost:7655;
        proxy_http_version 1.1;

        # Required for WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Proxy headers
        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;

        # Timeouts for WebSocket and adaptive polling (v4.24.0+)
        proxy_connect_timeout 7d;
        proxy_send_timeout 7d;
        proxy_read_timeout 7d;

        # Disable buffering for real-time updates
        proxy_buffering off;

        # IMPORTANT (v4.24.0+): Don't intercept error responses
        # This ensures 429 rate limit responses reach the client
        proxy_intercept_errors off;

        # Increase buffer sizes for large messages
        proxy_buffer_size 64k;
        proxy_buffers 8 64k;
        proxy_busy_buffers_size 128k;
    }
    
    # API endpoints (optional, same config as above)
    location /api/ {
        proxy_pass http://localhost:7655/api/;
        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_connect_timeout 7d;
        proxy_send_timeout 7d;
        proxy_read_timeout 7d;
        proxy_buffering off;
    }
}
```

## Caddy v2

Caddy automatically handles WebSocket upgrades when reverse proxying.

```caddy
pulse.example.com {
    reverse_proxy localhost:7655
}
```

For more control:

```caddy
pulse.example.com {
    reverse_proxy localhost:7655 {
        # Headers automatically handled by Caddy
        header_up Host {host}
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Proto {scheme}
        
        # Increase timeouts for WebSocket
        transport http {
            dial_timeout 30s
            response_header_timeout 30s
            read_timeout 0
        }
    }
}
```

## Apache

```apache
<VirtualHost *:443>
    ServerName pulse.example.com
    
    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem
    
    # Enable necessary modules:
    # a2enmod proxy proxy_http proxy_wstunnel headers
    
    # WebSocket proxy
    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://localhost:7655/$1" [P,L]
    
    # Regular HTTP proxy
    ProxyPass / http://localhost:7655/
    ProxyPassReverse / http://localhost:7655/
    
    # Preserve host headers
    ProxyPreserveHost On
    
    # Forward real IP
    RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
    RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
    RequestHeader set X-Forwarded-Proto "https"
    
    # Disable buffering
    ProxyIOBufferSize 65536
</VirtualHost>
```

## Traefik

```yaml
# docker-compose.yml
services:
  pulse:
    image: rcourtman/pulse:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.pulse.rule=Host(`pulse.example.com`)"
      - "traefik.http.routers.pulse.tls=true"
      - "traefik.http.services.pulse.loadbalancer.server.port=7655"
      # WebSocket support is automatic in Traefik 2.x+
```

Or using Traefik file configuration:

```yaml
# traefik-dynamic.yml
http:
  routers:
    pulse:
      rule: "Host(`pulse.example.com`)"
      service: pulse
      tls: {}
      
  services:
    pulse:
      loadBalancer:
        servers:
          - url: "http://localhost:7655"
```

## HAProxy

```haproxy
frontend https
    bind *:443 ssl crt /path/to/cert.pem
    
    # ACL for Pulse
    acl host_pulse hdr(host) -i pulse.example.com
    
    # WebSocket detection
    acl is_websocket hdr(Upgrade) -i websocket
    
    # Use backend
    use_backend pulse if host_pulse

backend pulse
    # Health check
    option httpchk GET /api/health
    
    # WebSocket support
    option http-server-close
    option forwardfor
    
    # Timeouts for WebSocket
    timeout client 3600s
    timeout server 3600s
    timeout tunnel 3600s
    
    # Backend server
    server pulse1 localhost:7655 check
```

## Cloudflare Tunnel

If using Cloudflare Tunnel (cloudflared):

```yaml
# config.yml
tunnel: YOUR_TUNNEL_ID
credentials-file: /path/to/credentials.json

ingress:
  - hostname: pulse.example.com
    service: http://localhost:7655
    originRequest:
      # Enable WebSocket
      noTLSVerify: false
      connectTimeout: 30s
      # No additional config needed - WebSockets work by default
  - service: http_status:404
```

## Testing WebSocket Connection

After configuring your reverse proxy, test that WebSockets work:

```bash
# Test basic connectivity
curl https://pulse.example.com/api/health

# Test WebSocket upgrade (should return 101 Switching Protocols)
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://pulse.example.com/api/ws
```

### v4.24.0+ Verification

**Test scheduler health API** (adaptive polling):
```bash
curl -s https://pulse.example.com/api/monitoring/scheduler/health | jq
```

**Expected response:**
- `enabled: true`
- Queue depth reasonable
- No stuck circuit breakers
- WebSocket connection remains open

**Verify rate limit headers** (v4.24.0+):
```bash
curl -I https://pulse.example.com/api/version
```

**Look for headers:**
```
HTTP/1.1 200 OK
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 499
X-RateLimit-Reset: 1698765432
```

**Test rate limiting:**
```bash
# Trigger rate limit (requires many requests)
for i in {1..600}; do curl -s https://pulse.example.com/api/health > /dev/null; done

# Should eventually see:
curl -I https://pulse.example.com/api/health
# HTTP/1.1 429 Too Many Requests
# X-RateLimit-Limit: 500
# X-RateLimit-Remaining: 0
# Retry-After: 60
```

**Important:** If you don't see rate limit headers or 429 responses are converted to 502/504, verify `proxy_intercept_errors off` (Nginx) or equivalent is set.

In browser console (F12):
```javascript
// Test WebSocket connection
const ws = new WebSocket('wss://pulse.example.com/api/ws');
ws.onopen = () => console.log('WebSocket connected!');
ws.onmessage = (e) => console.log('Received:', e.data);
ws.onerror = (e) => console.error('WebSocket error:', e);
```

## Common Issues

### "Connection Lost" or no real-time updates
- WebSocket upgrade not configured correctly
- Check proxy passes `Upgrade` and `Connection` headers
- Verify timeouts are increased for long connections

### CORS errors
- Pulse handles CORS internally
- Don't add additional CORS headers in proxy
- If needed, set `ALLOWED_ORIGINS` in Pulse configuration

### 502 Bad Gateway
- Pulse not running on expected port (default 7655)
- Check with: `curl http://localhost:7655/api/health`
- Verify Pulse service: `systemctl status pulse` (use `pulse-backend` if you're on a legacy unit)

### WebSocket closes immediately
- Timeout too short in proxy configuration
- Increase `proxy_read_timeout` (Nginx) or equivalent
- Set to at least 3600s (1 hour) or more

## Security Recommendations

1. **Always use HTTPS** for production deployments
2. **Set proper headers** to prevent clickjacking:
   ```nginx
   add_header X-Frame-Options "SAMEORIGIN";
   add_header X-Content-Type-Options "nosniff";
   ```
3. **Rate limiting** for API endpoints:
   ```nginx
   limit_req_zone $binary_remote_addr zone=pulse:10m rate=30r/s;
   limit_req zone=pulse burst=50 nodelay;
   ```
4. **Hide proxy version**:
   ```nginx
   proxy_hide_header X-Powered-By;
   server_tokens off;
   ```

## Support

If WebSockets still don't work after following this guide:
1. Check browser console for errors (F12)
2. Verify Pulse logs: `journalctl -u pulse -f`
3. Test without proxy first: `http://your-server:7655`
4. Report issues: https://github.com/rcourtman/Pulse/issues
