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. OpenAPI (/docs, /redoc, # /openapi.json) is intentionally NOT proxied. # # Paths that are BOTH React routes (/jobs, /inbox, …) and API prefixes must # require a sub-path: otherwise a cold open / refresh of /jobs is stolen by # the proxy and returns a FastAPI 404 instead of index.html. # SPA page roots that also prefix API calls — sub-path required. location ~ ^/(jobs|inbox|pipeline|tasks|assessments|offers|managers|analytics|notifications)/ { 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; proxy_request_buffering off; } # API-only prefixes (no SPA page at the bare path). location ~ ^/(health|users|roles|permissions|permission-tags|email|job|candidate|notes|interview|feedback|activity|org-settings|saved-searches|search|documents|sheet)(/|$) { 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; proxy_request_buffering off; } # 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; }