Reverse proxy and TLS
Kryton’s HTTP server (Fastify, listens on PORT, default 3001) is
designed to sit behind a TLS-terminating reverse proxy in production.
Trust-proxy is on
Section titled “Trust-proxy is on”packages/server/src/app.ts builds Fastify with trustProxy: true, so
the server honours X-Forwarded-For / X-Forwarded-Proto /
X-Forwarded-Host from any upstream. Only run Kryton behind a proxy
you control — trustProxy: true means the server trusts whatever sent
those headers.
What the proxy must do
Section titled “What the proxy must do”- Terminate TLS.
- Forward HTTP to Kryton’s
PORT. - Pass through the WebSocket upgrade on
/ws/yjs/:docId(collaborative editing) and any plugin WebSocket routes. - Match the public URL you configured in
APP_URL/BETTER_AUTH_URL— better-auth checks origins against these. - If you use passkeys, set
WEBAUTHN_RP_IDto the public hostname without port or scheme, e.g.kryton.example.com.
Caddy handles TLS, HTTP/2, and WebSocket upgrades with no extra configuration.
kryton.example.com { reverse_proxy localhost:3001}Nginx needs the WebSocket upgrade map and proxy_http_version 1.1:
map $http_upgrade $connection_upgrade { default upgrade; '' close;}
server { listen 443 ssl http2; server_name kryton.example.com;
ssl_certificate /etc/letsencrypt/live/kryton.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/kryton.example.com/privkey.pem;
client_max_body_size 50m;
location / { proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $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;
# Yjs WebSockets are long-lived. Without this the proxy will cut # collaborative editing sessions after 60 s of idle. proxy_read_timeout 3600s; proxy_send_timeout 3600s; }}Traefik (labels on the Kryton container)
Section titled “Traefik (labels on the Kryton container)”labels: - traefik.enable=true - traefik.http.routers.kryton.rule=Host(`kryton.example.com`) - traefik.http.routers.kryton.entrypoints=websecure - traefik.http.routers.kryton.tls.certresolver=letsencrypt - traefik.http.services.kryton.loadbalancer.server.port=3001Traefik proxies WebSockets transparently — no extra middleware required.
Yjs WebSocket — what to forward
Section titled “Yjs WebSocket — what to forward”The collaborative-editing route is mounted at /ws/yjs/:docId (see
packages/server/src/modules/collab/ws/yjs.handler.ts). The upgrade
request:
- Method:
GETwith the standardUpgrade: websocket/Connection: Upgradeheaders. - Auth: session cookie or an agent bearer token passed in
Sec-WebSocket-Protocolas the pairkryton-token, <token>(packages/server/src/lib/ws-auth.ts). Query-string tokens are deliberately rejected because they leak through access logs.
Any proxy that preserves the Cookie and Sec-WebSocket-Protocol
headers (all three examples above do) works as-is.
Production checklist
Section titled “Production checklist”APP_URLandBETTER_AUTH_URLset to your publichttps://…origin.CORS_ORIGINSincludes your public origin and nothing else.BETTER_AUTH_SECRETis at least 32 random characters and stable across restarts.WEBAUTHN_RP_IDset to the public hostname if you use passkeys.- Reverse-proxy WebSocket timeouts ≥ 1 hour so live editing sessions don’t get cut.