Skip to content

perf: add long-lived cache headers for content-hashed static assets#747

Merged
Aureliolo merged 2 commits intomainfrom
perf/nginx-cache-headers
Mar 22, 2026
Merged

perf: add long-lived cache headers for content-hashed static assets#747
Aureliolo merged 2 commits intomainfrom
perf/nginx-cache-headers

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Add location /assets/ with Cache-Control: public, max-age=31536000, immutable for Vite's content-hashed JS/CSS output -- uses prefix match instead of regex to avoid caching unhashed public/ files like favicon.svg
  • Add location = /index.html with Cache-Control: no-cache so browsers always revalidate the entry point and pick up new deployments (304s keep it fast)
  • Extract 6 security headers into web/security-headers.conf include snippet to avoid duplication across location blocks (nginx does not inherit add_header from server block into location blocks that define their own)
  • Add charset utf-8 to server block
  • Add proxy_http_version 1.1 to API proxy block (consistency with WebSocket block, enables keepalive to backend)

Test plan

  • Docker image builds successfully (docker compose -f docker/compose.yml build web)
  • curl -I /assets/index-*.js returns Cache-Control: public, max-age=31536000, immutable + all 6 security headers
  • curl -I /assets/index-*.css returns same cache + security headers
  • curl -I / returns Cache-Control: no-cache + all 6 security headers
  • curl -I /nonexistent-route (SPA fallback) returns Cache-Control: no-cache + all 6 security headers
  • curl -I /favicon.svg (unhashed) returns security headers but NO long cache
  • Hadolint passes on Dockerfile changes (CI)

Review coverage

Pre-reviewed by 3 agents (docs-consistency, infra-reviewer, issue-resolution-verifier). 0 findings -- all checks pass, all acceptance criteria resolved.

Closes #686

- Add location /assets/ with Cache-Control: public, max-age=1y, immutable
  for Vite's content-hashed JS/CSS output (prefix match avoids caching
  unhashed public/ files like favicon.svg)
- Add location = /index.html with Cache-Control: no-cache so browsers
  always revalidate the entry point and pick up new deployments
- Extract 6 security headers into web/security-headers.conf include
  snippet to avoid duplication across location blocks (nginx does not
  inherit add_header from server block into location blocks)
- Add charset utf-8 to server block
- Add proxy_http_version 1.1 to API proxy (consistency with WebSocket
  block, enables keepalive to backend)

Closes #686

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 22, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the Nginx configuration for the web application, primarily focusing on optimizing client-side caching, streamlining security header management, and improving API proxy behavior. These changes aim to boost application performance by reducing unnecessary network requests for static assets, ensure fresh content delivery for the main entry point, and maintain robust security practices across different content types.

Highlights

  • Cache Headers for Static Assets: Implemented long-lived "Cache-Control: public, max-age=31536000, immutable" headers for content-hashed JavaScript and CSS assets served from /assets/ to leverage browser caching effectively.
  • index.html Caching: Configured "Cache-Control: no-cache" for /index.html to ensure browsers always revalidate the entry point, allowing for quick updates upon new deployments.
  • Security Header Refactoring: Extracted six common security headers into a dedicated web/security-headers.conf file, promoting reusability and avoiding duplication across Nginx location blocks.
  • Nginx Configuration Enhancements: Added "charset utf-8" to the Nginx server block and "proxy_http_version 1.1" to the API proxy block for consistency and improved backend connection management.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81aff301-d559-4010-82db-77244824fd27

📥 Commits

Reviewing files that changed from the base of the PR and between 45b0743 and 3387dc1.

📒 Files selected for processing (3)
  • docs/security.md
  • web/nginx.conf
  • web/security-headers.conf
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Analyze (python)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to docker/Dockerfile.web : `nginxinc/nginx-unprivileged` base with non-root user (UID 101), Vue 3 SPA (PrimeVue + Tailwind CSS), SPA routing, API/WebSocket proxy to backend
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).

Applied to files:

  • docs/security.md
📚 Learning: 2026-03-22T16:44:15.487Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-22T16:44:15.487Z
Learning: Security scanning: gitleaks (push/PR + weekly), zizmor (workflow analysis), OSSF Scorecard (weekly), Socket.dev (PR supply chain), ZAP DAST (weekly + manual).

Applied to files:

  • docs/security.md
📚 Learning: 2026-03-22T16:44:15.487Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-22T16:44:15.487Z
Learning: Applies to .github/{.trivyignore.yaml,.grype.yaml} : CVE triage rules in `.github/.trivyignore.yaml` and `.github/.grype.yaml`.

Applied to files:

  • docs/security.md
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to docker/Dockerfile.web : `nginxinc/nginx-unprivileged` base with non-root user (UID 101), Vue 3 SPA (PrimeVue + Tailwind CSS), SPA routing, API/WebSocket proxy to backend

Applied to files:

  • web/nginx.conf
🔇 Additional comments (6)
web/security-headers.conf (1)

1-10: LGTM! Security headers are comprehensive and well-documented.

The CSP correctly includes the hardening directives (object-src 'none', base-uri 'self', form-action 'self'), and the inline comments explain the rationale for 'unsafe-inline' and connect-src WebSocket coverage.

One optional enhancement: consider adding preload to HSTS for inclusion in browser preload lists, though this requires registering the domain at hstspreload.org and ensuring all subdomains support HTTPS.

docs/security.md (2)

92-92: LGTM!

The Cache-Control documentation accurately reflects the path-aware caching strategy implemented in nginx.conf.


234-243: LGTM!

The expanded DAST tuning explanation clearly documents the rationale for each caching tier, which helps future maintainers understand why rule 10049 is configured as warn-only.

web/nginx.conf (3)

10-10: LGTM!

Adding charset utf-8 ensures proper Content-Type headers for text responses.


23-40: LGTM! Location block structure and header inheritance are correct.

The implementation correctly handles nginx's add_header inheritance behavior:

  • Server-level include provides headers for location / (which has no own add_header)
  • Location-specific includes are added where custom add_header directives exist

The exact-match location = /index.html correctly receives requests from both the index directive and try_files fallbacks, ensuring SPA routes get no-cache.


64-76: No action needed. All required security headers are properly set by the backend middleware (src/synthorg/api/middleware.py). The nginx API proxy configuration correctly delegates header responsibility to the backend, which implements:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Strict-Transport-Security (HSTS): max-age=63072000; includeSubDomains
  • Permissions-Policy: geolocation=(), camera=(), microphone=()
  • Content-Security-Policy: path-aware configuration (strict for /api/)

This approach is appropriate and avoids header duplication at the proxy layer.


Walkthrough

This change refactors nginx configuration to extract security headers into a separate included file and adds explicit cache-control directives. The Dockerfile now copies a new web/security-headers.conf file containing security headers like Content-Security-Policy, Strict-Transport-Security, and others. The main web/nginx.conf was updated to include this file and define specific cache policies: /assets/ receives a one-year max-age cache header for static content, while /index.html receives a no-cache directive. The API proxy configuration was also updated to explicitly specify HTTP version 1.1.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding long-lived cache headers for content-hashed static assets, which is the primary focus of the changeset.
Description check ✅ Passed The description is directly related to the changeset, detailing the specific cache control strategies, security header extraction, and test coverage.
Linked Issues check ✅ Passed All acceptance criteria from #686 are met: hashed assets have long cache headers, security headers are preserved via include snippet, and index.html uses no-cache policy.
Out of Scope Changes check ✅ Passed All changes directly align with #686 objectives: caching strategy for hashed assets, security header extraction, index.html cache policy, charset addition, and proxy improvements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces performance improvements by configuring caching headers for static assets and the main entrypoint file. It correctly uses long-lived caches for content-hashed assets and forces revalidation for index.html. The refactoring of security headers into a separate file is a good improvement for maintainability. I have one suggestion to complete the implementation of keepalive connections for the API proxy, which was one of the stated goals of the changes.

# API proxy to backend service
location /api/ {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To enable HTTP keepalive connections to the upstream server, you also need to clear the Connection header. The proxy_http_version 1.1; directive is not sufficient on its own, as nginx will otherwise send a Connection: close header by default in proxied requests. Adding proxy_set_header Connection ""; will complete the keepalive implementation and improve performance by reusing connections to the backend.

        proxy_http_version 1.1;
        proxy_set_header Connection "";

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/security-headers.conf`:
- Line 9: Update the Content-Security-Policy header declaration (the add_header
line that sets Content-Security-Policy) to include additional hardening
directives: add object-src 'none' to block plugins, base-uri 'self' to prevent
<base> tag injection, and form-action 'self' to restrict form submissions; keep
existing directives intact and ensure the full policy string remains quoted and
applied with the always flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 93ee353c-6017-4558-8d59-03aa8956c7d6

📥 Commits

Reviewing files that changed from the base of the PR and between 289638f and 45b0743.

📒 Files selected for processing (3)
  • docker/web/Dockerfile
  • web/nginx.conf
  • web/security-headers.conf
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Analyze (python)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to docker/Dockerfile.web : `nginxinc/nginx-unprivileged` base with non-root user (UID 101), Vue 3 SPA (PrimeVue + Tailwind CSS), SPA routing, API/WebSocket proxy to backend
📚 Learning: 2026-03-15T18:17:43.675Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.

Applied to files:

  • docker/web/Dockerfile
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to docker/Dockerfile.web : `nginxinc/nginx-unprivileged` base with non-root user (UID 101), Vue 3 SPA (PrimeVue + Tailwind CSS), SPA routing, API/WebSocket proxy to backend

Applied to files:

  • docker/web/Dockerfile
  • web/nginx.conf
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to docker/Dockerfile : Docker: 3-stage build (builder → setup → distroless runtime) for backend, Chainguard Python, non-root (UID 65532), CIS-hardened

Applied to files:

  • docker/web/Dockerfile
🔇 Additional comments (8)
web/nginx.conf (6)

10-10: LGTM!

Adding charset utf-8 at server level ensures consistent UTF-8 encoding for all text responses.


23-27: LGTM!

Good documentation of nginx's add_header inheritance behavior and clean extraction to an include file.


29-33: LGTM!

Exact-match location for /index.html with no-cache ensures browsers revalidate the entry point on each visit, correctly picking up new hashed asset references after deployments.


35-40: LGTM!

Prefix-match for /assets/ correctly targets Vite's content-hashed output. The immutable directive combined with one-year max-age is ideal for content-hashed assets—browsers won't revalidate until the hash changes.


67-67: LGTM!

Adding proxy_http_version 1.1 aligns the API proxy with the WebSocket block and enables HTTP/1.1 features like connection keepalive to the backend.


42-45: The current configuration already provides the correct cache behavior for SPA fallback routes.

When try_files internally redirects to /index.html, nginx restarts location matching and the exact match location = /index.html is evaluated for the redirected request. Therefore, the Cache-Control: no-cache header from that location block is applied to all index.html responses, including SPA fallback paths. No changes are needed.

docker/web/Dockerfile (1)

33-35: LGTM!

The security headers configuration file is correctly copied to /etc/nginx/security-headers.conf, matching the include path used in nginx.conf. Grouping config file copies together maintains good organization.

web/security-headers.conf (1)

1-10: Well-structured security headers with good documentation.

The extraction of security headers into a shared snippet is clean and maintainable. The CSP correctly accommodates PrimeVue's need for inline styles while maintaining strict controls elsewhere.

…agents

- Add proxy_set_header Connection "" to API proxy for proper keepalive
  (Gemini: proxy_http_version 1.1 alone sends Connection: close)
- Harden CSP with object-src 'none', base-uri 'self', form-action 'self'
  (CodeRabbit: reduces attack surface for plugins, base tag injection,
  form submission targets)
- Update docs/security.md Cache-Control table and prose to document
  the new 3-tier dashboard caching strategy (docs-consistency agent)

Pre-reviewed by 3 local agents + 2 external reviewers, 3 findings addressed

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 22, 2026 18:55 — with GitHub Actions Inactive
@Aureliolo Aureliolo merged commit 4d350b5 into main Mar 22, 2026
31 checks passed
@Aureliolo Aureliolo deleted the perf/nginx-cache-headers branch March 22, 2026 19:01
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 22, 2026 19:01 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Mar 22, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.4.8](v0.4.7...v0.4.8)
(2026-03-22)


### Features

* add auto_cleanup config and improve update UX
([#741](#741))
([289638f](289638f))
* add reporting lines, escalation paths, and workflow handoffs to
templates ([#745](#745))
([c374cc9](c374cc9))
* differentiate template operational configs
([#742](#742))
([9b48345](9b48345))
* diversify personality preset assignments across templates
([#743](#743))
([15487a5](15487a5))
* improve template metadata -- skill taxonomy, descriptions, tags, and
display names ([#752](#752))
([f333f24](f333f24))


### Bug Fixes

* resolve log analysis findings (Ollama prefix, logging, init)
([#748](#748))
([8f871a4](8f871a4))
* use git tag for dev release container image tags
([#749](#749))
([f30d071](f30d071))
* use subordinate_id/supervisor_id in HierarchyResolver
([#751](#751))
([118235b](118235b))


### Performance

* add long-lived cache headers for content-hashed static assets
([#747](#747))
([4d350b5](4d350b5))
* use worksteal distribution for pytest-xdist
([#750](#750))
([b7dd7de](b7dd7de))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: add long-lived cache headers for content-hashed static assets in nginx

1 participant