# Security Headers, Coverage Gates & Web Vitals Design > **Date:** 2026-03-18 > **Product:** ResolutionFlow > **Branch:** `feat/security-headers-coverage-performance` > **Purpose:** Add HTTP security headers, enforce test coverage gates, and instrument Core Web Vitals reporting --- ## Decisions | Decision | Choice | Rationale | |----------|--------|-----------| | Security headers priority | First | Highest trust signal for MSP buyers, real protection | | CSP rollout strategy | Report-only first, then enforce | Avoids breaking third-party integrations (PostHog, Sentry, Google Fonts, React Flow) | | Backend coverage gate | 80% fail threshold | Already near this level, prevents drift | | Frontend coverage gate | Report-only (no gate yet) | Starting from zero — establish baseline first | | Web Vitals destination | PostHog | 100% of sessions captured (vs Sentry's 20% sample), correlate with product analytics | --- ## 1. Security Headers Middleware ### New file: `backend/app/core/security_headers.py` Starlette middleware that adds security headers to every response. ### Headers | Header | Value | Purpose | |--------|-------|---------| | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS for 1 year | | `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing | | `X-Frame-Options` | `DENY` | Block iframe embedding | | `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage | | `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Disable unused browser APIs | | `Content-Security-Policy-Report-Only` | *(see below)* | CSP in report-only mode | ### CSP Directive (report-only) ``` default-src 'self'; script-src 'self' https://us.i.posthog.com https://*.sentry.io; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://us.posthog.com https://us.i.posthog.com https://*.sentry.io https://api.resolutionflow.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self' ``` ### Wiring - Add middleware in `main.py` after CORS middleware (so CORS preflight responses aren't affected) - CSP directives configurable via `config.py` so promotion to enforcing mode is a config change, not a code change - HSTS only sent when `DEBUG=false` (avoid locking localhost into HTTPS) ### Tests Integration test that hits an endpoint and asserts all expected headers are present with correct values. --- ## 2. Coverage Gates ### Backend: Enforce at 80% - Add `--cov-fail-under=80` to the pytest command in CI - One-line change — reporting already wired up with `pytest-cov` ### Frontend: Report-only (establish baseline) - Install `@vitest/coverage-v8` as dev dependency - Add coverage config to `vite.config.ts`: - Reporters: `text` + `json-summary` + `html` - Include: `src/**/*.{ts,tsx}` - Exclude: `src/test/`, `src/types/`, `**/*.d.ts` - Add `test:coverage` script to `package.json` - Update CI to run `npm run test:coverage` instead of `npm test` - Display summary in CI output — no failure threshold yet - Add `coverage/` to `.gitignore` --- ## 3. Web Vitals → PostHog ### Install `web-vitals` npm package. ### New file: `frontend/src/lib/webVitals.ts` - Import `onLCP`, `onINP`, `onCLS`, `onFCP`, `onTTFB` from `web-vitals` - Each callback sends a PostHog event (`web_vitals`) with properties: - `metric_name` — LCP, INP, CLS, FCP, TTFB - `metric_value` — numeric value - `metric_rating` — good / needs-improvement / poor - `page_path` — current route - Single `initWebVitals()` function that registers all observers ### Wiring Call `initWebVitals()` in `main.tsx` after PostHog initialization. --- ## Scope Summary | Area | Scope | Files | |------|-------|-------| | Security headers | New middleware + config + test | 3-4 backend files | | Coverage gates | CI config + vitest coverage setup | CI workflow + 3 frontend config files | | Web Vitals | New lib + dependency + main.tsx wiring | 2-3 frontend files | Small, contained changes across all three. No architectural changes or new database models.