server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # 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. backend-api is the compose service name. location ~ ^/(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|docs|openapi\.json|redoc)(/|$) { 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; } # One SPA at `/`. `vite dev` and `vite preview` serve index.html for every # unmatched path; vite.config.js notes that a static deploy needs the equivalent # rewrite rule. This is it — without it /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"; } # index.html must never be cached, or a redeploy keeps serving the old asset hashes. location = /index.html { add_header Cache-Control "no-store"; } gzip on; gzip_min_length 1024; gzip_types text/css text/javascript application/javascript application/json image/svg+xml; }