diff --git a/docs/REVERSE_PROXY.md b/docs/REVERSE_PROXY.md
index 288aeb21..b0c368d8 100644
--- a/docs/REVERSE_PROXY.md
+++ b/docs/REVERSE_PROXY.md
@@ -290,6 +290,48 @@ Also enable `Websockets Support` in Nginx Proxy Manager for this host.
- the proxy is compressing and buffering live traffic
- the proxy is missing WebSocket support
-## Website docs
+## Example: Caddy
-- Website version: `packages/docs/content/docs/reverse-proxy.mdx`
+
+Show example config
+
+```caddy
+reverse_proxy 127.0.0.1:3000 {
+ # WebSocket support is automatic in Caddy
+
+ # Flush SSE responses immediately
+ flush_interval -1
+
+ # Pass through Host and proxy headers
+ header_up Host {host}
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+
+ # Increase timeouts for long-lived streams
+ transport http {
+ read_timeout 3600s
+ write_timeout 3600s
+ }
+}
+```
+
+
+
+Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering.
+
+## CDN and double-compression warning
+
+If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression:
+
+- OpenChamber compresses HTTP responses with gzip (threshold 1 KB).
+- Cloudflare and other CDNs also compress responses by default.
+- This can cause double-compressed responses or incorrect `Content-Encoding` headers.
+
+To avoid this, disable compression at **one** layer:
+
+- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode).
+- **Nginx:** `gzip off` (already shown in the examples above).
+- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content.
+
+SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths.
diff --git a/packages/docs/content/docs/reverse-proxy.mdx b/packages/docs/content/docs/reverse-proxy.mdx
index 3951e35d..4085690a 100644
--- a/packages/docs/content/docs/reverse-proxy.mdx
+++ b/packages/docs/content/docs/reverse-proxy.mdx
@@ -295,6 +295,52 @@ Also enable `Websockets Support` in Nginx Proxy Manager for this host.
- the proxy is compressing and buffering live traffic
- the proxy is missing WebSocket support
+## Example: Caddy
+
+
+Show example config
+
+```caddy
+reverse_proxy 127.0.0.1:3000 {
+ # WebSocket support is automatic in Caddy
+
+ # Flush SSE responses immediately
+ flush_interval -1
+
+ # Pass through Host and proxy headers
+ header_up Host {host}
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+
+ # Increase timeouts for long-lived streams
+ transport http {
+ read_timeout 3600s
+ write_timeout 3600s
+ }
+}
+```
+
+
+
+Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering.
+
+## CDN and double-compression warning
+
+If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression:
+
+- OpenChamber compresses HTTP responses with gzip (threshold 1 KB).
+- Cloudflare and other CDNs also compress responses by default.
+- This can cause double-compressed responses or incorrect `Content-Encoding` headers.
+
+To avoid this, disable compression at **one** layer:
+
+- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode).
+- **Nginx:** `gzip off` (already shown in the examples above).
+- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content.
+
+SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths.
+
## Related
- [Tunnels](/tunnels/)
diff --git a/packages/web/server/index.js b/packages/web/server/index.js
index c3a5f8fa..4a5aea38 100644
--- a/packages/web/server/index.js
+++ b/packages/web/server/index.js
@@ -109,11 +109,37 @@ function headerIncludesEventStream(value) {
return false;
}
+/**
+ * SSE endpoint paths that must never be compressed by the compression middleware.
+ *
+ * The compression middleware filter runs before route handlers, so
+ * `res.getHeader('Content-Type')` is still undefined at that point.
+ * This means the Accept-header check alone is not sufficient for
+ * non-standard clients (e.g. curl, fetch) that omit Accept.
+ * Path-based exclusion acts as a deterministic fallback.
+ */
+const SSE_PATH_PREFIXES = [
+ '/api/event',
+ '/api/global/event',
+ '/api/notifications/stream',
+ '/api/openchamber/events',
+];
+
function shouldSkipCompression(req, res) {
if (headerIncludesEventStream(req.headers.accept)) {
return true;
}
+ const pathname = req.path || req.url || '';
+ if (pathname.startsWith('/api/terminal/') && pathname.endsWith('/stream')) {
+ return true;
+ }
+ for (const prefix of SSE_PATH_PREFIXES) {
+ if (pathname === prefix) {
+ return true;
+ }
+ }
+
return headerIncludesEventStream(res.getHeader('Content-Type'));
}