{$DOMAIN} {
	# Compress everything EXCEPT the SSE stream — gzip buffering delays
	# "real-time" likes/comments until the ~30s keep-alive tick.
	@compressible not path /api/v1/stream
	encode @compressible zstd gzip

	# Site-wide security headers (defense-in-depth). HSTS is free since Caddy
	# already terminates TLS. nosniff also covers all of /media/*.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		Referrer-Policy "strict-origin-when-cross-origin"
	}

	# SvelteKit frontend — static assets with long-lived cache (content-hashed filenames)
	@hashed_assets path_regexp hashed /_app/immutable/.*\.[a-f0-9]{8,}\.(js|css|woff2)$
	header @hashed_assets Cache-Control "public, max-age=31536000, immutable"

	# Preview/thumbnail images. These are now served by the app through a
	# visibility-checked alias (/api/v1/upload/{id}/{preview,thumbnail}) so moderation
	# can revoke access; direct /media/previews|thumbnails is 404-blocked at the app.
	# Privately cacheable for a short window (the app sets the same header; this is the
	# edge carve-out from the blanket no-store below). Kept short so a moderated image
	# stops being served to a direct-URL holder promptly.
	@media_api path /api/v1/upload/*/preview /api/v1/upload/*/thumbnail
	header @media_api Cache-Control "private, max-age=300"

	# API — never cache, EXCEPT the gated image routes above.
	@api {
		path /api/*
		not path /api/v1/upload/*/preview /api/v1/upload/*/thumbnail
	}
	header @api Cache-Control "no-store"

	# Route API and media requests to the Rust backend
	reverse_proxy /api/* app:3000
	reverse_proxy /media/* app:3000

	# Everything else goes to SvelteKit frontend
	reverse_proxy frontend:3001
}
