# nginx config for serving SecureBit.chat (static PWA) on Fly.io. # Mirrors the behavior of the Apache .htaccess: correct JS MIME for ES modules # (.jsx/.mjs), no-cache for the app shell / service worker / versioning files, # long-immutable cache for hashed/static assets, security headers, SPA fallback. worker_processes auto; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # ES modules must be served as JavaScript. nginx's default mime.types maps # .js but not .mjs/.jsx — declare them explicitly (this overrides .js too). types { application/javascript js mjs jsx; text/css css; application/json json map; application/manifest+json webmanifest; font/woff2 woff2; font/woff woff; image/svg+xml svg; } sendfile on; tcp_nopush on; server_tokens off; gzip on; gzip_vary on; gzip_min_length 256; gzip_types text/plain text/css application/javascript application/json image/svg+xml font/woff2; # Decide Cache-Control from the request path. Keeping all add_header calls at # one level avoids nginx's header-inheritance reset between blocks. map $uri $sb_cache { default "public, max-age=31536000, immutable"; ~^/index\.html$ "no-cache, no-store, must-revalidate"; ~^/$ "no-cache, no-store, must-revalidate"; ~^/sw\.js$ "no-cache, no-store, must-revalidate"; ~^/manifest\.json$ "no-cache, no-store, must-revalidate"; ~^/meta\.json$ "no-cache, no-store, must-revalidate"; ~^/dist/ "no-cache, no-store, must-revalidate"; } server { listen 8080; listen [::]:8080; server_name _; root /usr/share/nginx/html; index index.html; # Security headers (frame-ancestors complements the in-page CSP meta tag). add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "DENY" always; add_header Content-Security-Policy "frame-ancestors 'none';" always; add_header Cache-Control $sb_cache always; add_header Service-Worker-Allowed "/" always; # Real asset files must return 404 when missing — never fall back to the # HTML shell, which would be served with the wrong content type and break # module/script loading (e.g. a missing config/ice-servers.js). location ~* \.(js|mjs|jsx|css|json|map|woff2?|ttf|otf|png|jpe?g|gif|webp|svg|ico|mp3|mp4|webm)$ { try_files $uri =404; } # SPA-style fallback so unknown navigation routes still load the app shell. location / { try_files $uri $uri/ /index.html; } } }