server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # CV / multipart uploads (MAX_PDF_SIZE_MB is 10; leave headroom for form fields). client_max_body_size 25m; # Security headers on every response. add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; # Same-origin API proxy. The SPA is built with an empty VITE_API_BASE so # fetch('/jobs/fetch') stays on this host; without this block nginx would # serve index.html for those paths (200 HTML) and the Jobs board would # parse an empty payload. OpenAPI (/docs, /redoc, /openapi.json) is # intentionally NOT proxied — keep the schema off the public edge. location ~ ^/(health|users|roles|permissions|permission-tags|managers|inbox|email|job|jobs|candidate|notes|interview|feedback|activity|pipeline|notifications|analytics|offers|tasks|assessments|org-settings|saved-searches|search)(/|$) { proxy_pass http://backend-api:8000; proxy_http_version 1.1; 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_set_header Authorization $http_authorization; proxy_connect_timeout 10s; proxy_send_timeout 120s; proxy_read_timeout 120s; } # One SPA at `/`. Without this, /auth/confirm-email (a router path, not a # file) 404s when a confirmation email link is opened cold. location / { try_files $uri $uri/ /index.html; } # Hashed filenames, so they can be cached hard. location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; } # index.html must never be cached, or a redeploy keeps serving the old asset hashes. location = /index.html { add_header Cache-Control "no-store"; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; } gzip on; gzip_min_length 1024; gzip_types text/css text/javascript application/javascript application/json image/svg+xml; }