# How do I add a Content Security Policy without breaking my app?

## Context for the AI tool

You are adding a Content Security Policy (CSP) to this app without breaking any feature.

- Do not ship an enforcing policy. Start with `Content-Security-Policy-Report-Only` and keep it
  that way until I confirm, after at least a week of real traffic without unexplained violations.
- Before writing the policy, list every external origin the app loads from and report it as a
  table: origin, what loads from it (script, style, font, image, frame, connection), which feature
  needs it.
- Never add `'unsafe-eval'`, and never allow `*` or a whole scheme such as `https:` to make a
  violation go away. Ask me instead.
- Do not log full URLs from violation reports. Log the directive, the blocked origin and the page
  path only: URLs can contain visitor data in query strings.

## 1. List what the app loads

**What to look for:** every origin the browser contacts. Open each main page (home, sign-up,
log-in, the main screen, checkout or payment) with the browser's Network tab open, and sort by
domain. Typical entries: your own API and storage (for example `<project>.supabase.co`), payment
(`js.stripe.com`), analytics and ads (Google Tag Manager, Google Analytics, Meta), fonts
(`fonts.googleapis.com`, `fonts.gstatic.com`), embeds (YouTube, maps, Calendly), chat widgets.

```bash
grep -rhoE "https://[a-zA-Z0-9.-]+" --include=*.{ts,tsx,js,jsx,svelte,vue,html} src app pages components 2>/dev/null | sort -u
```

**How to fix:** nothing yet. Combine the Network tab and the grep into the origin table.

**How to verify:** every row has a feature next to it. Origins nobody can explain are removed
from the code, not added to the policy.

## 2. Write a report-only policy

**What to look for:** where the app sets HTTP headers: the framework (`next.config.js`
`headers()`, SvelteKit `kit.csp` or a `handle` hook), or the host (`vercel.json`, `_headers` on
Netlify, `customHttp.yml` on AWS Amplify).

**How to fix:** set one `Content-Security-Policy-Report-Only` header built from the table. A
starting point to adjust:

```
default-src 'self';
script-src 'self' https://js.stripe.com https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://<project>.supabase.co wss://<project>.supabase.co;
frame-src https://js.stripe.com;
object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';
report-uri /csp-report
```

If the framework can add nonces or hashes for its own inline scripts (SvelteKit `kit.csp`,
Next.js middleware with a nonce), use that instead of `'unsafe-inline'` in `script-src`.

**How to verify:** the header is present on every page (`curl -sI https://<your-app>/ | grep -i content-security`)
and the app still works exactly as before, because report-only blocks nothing.

## 3. Collect violations from real visitors

**What to look for:** an endpoint for `report-uri` (and optionally `report-to`) that stores the
reports somewhere you will actually read.

**How to fix:** add a small POST endpoint, for example `/csp-report`, that accepts both formats
(`report-uri` sends `{"csp-report": {...}}`, the Reporting API sends an array of
`{type, body}`), logs one line per violation with directive, blocked origin and page path, and
returns `204`. Limit the body size and don't require authentication on it.

**How to verify:** trigger a violation yourself (load a page with a test image from an origin
that is not allowed) and find the log line. Check the logs really are kept: on some hosts the
server function has no log storage until you turn it on, and then every report disappears.

## 4. Read a week of reports

**What to look for:** each violation that comes from a real feature. A common one: an ad or
analytics tag that pings a second host only after a conversion, so it never shows up when you
browse the site yourself.

**How to fix:** add the origin to the narrowest directive that needs it, with a note saying
which feature it is for. Ignore noise from browser extensions (`chrome-extension://`,
`moz-extension://`) and translation tools.

**How to verify:** after a full week, including a real purchase or sign-up, no violations
remain that belong to a feature.

## 5. Enforce

**How to fix:** rename the header to `Content-Security-Policy` and keep `report-uri`, so new
violations still reach you after a third-party script changes.

**How to verify:** run through sign-up, log-in, payment, analytics and every embed in a private
window with the console open: no CSP errors. Check that your analytics and ad conversions keep
arriving for the next days. A free scan on securityheaders.com shows the header as present.

---

Still stuck? WDDW fixes it. Book a free 15-minute call:
https://www.wedodevwork.com/en/contact-us?utm_source=fixes&utm_medium=md&utm_campaign=csp-without-breaking-your-app

Did this help? An honest review helps us a lot: https://g.page/r/CXmlHMBM4uxyEBM/review
More free fixes: https://www.wedodevwork.com/en/fixes · Star the repo: https://github.com/WEDODEVWORK/fixes
