Documentation

Helix developer docs

REST endpoints, webhook signing, deployment checklists, and platform guarantees.

CSP Violation Reports

Production responses ship with both legacy (report-uri) and modern (report-to + Reporting-Endpoints) CSP reporting. Browsers POST violations to /api/public/csp-report, which writes them to the csp_reports table.

  • Endpoint: src/routes/api/public/csp-report.ts (uses supabaseAdmin)
  • Table: public.csp_reports (RLS — only owner-role users can read)
  • Wiring: src/lib/security-headers.ts adds report-uri, report-to, and the Reporting-Endpoints header. Reporting is production-only — dev responses skip it to avoid noise from HMR.

Why we collect them

CSP changes can silently break features (a new third-party script, an inline style added by a dependency, a redirect to a new auth provider). Reports let us catch these regressions in production without users telling us.

Reviewing reports

You need an owner role on at least one tenant. Run via Lovable Cloud → Database → SQL editor (or any psql session):

1. What's broken right now? (last 24 h)

SELECT
  effective_directive,
  blocked_uri,
  COUNT(*) AS hits,
  MAX(received_at) AS last_seen
FROM public.csp_reports
WHERE received_at > now() - INTERVAL '24 hours'
GROUP BY effective_directive, blocked_uri
ORDER BY hits DESC
LIMIT 50;

2. Drill into a specific violation

SELECT received_at, document_uri, source_file, line_number, user_agent, raw
FROM public.csp_reports
WHERE blocked_uri = 'https://example.com/some-script.js'
ORDER BY received_at DESC
LIMIT 20;

3. Per-directive volume (last 7 days)

SELECT effective_directive, COUNT(*) AS hits
FROM public.csp_reports
WHERE received_at > now() - INTERVAL '7 days'
GROUP BY effective_directive
ORDER BY hits DESC;

Triage playbook

For each high-volume violation, decide one of:

DecisionWhenAction
AllowLegitimate dependency we just added (e.g. new font CDN, OAuth provider).Add the source to the matching directive in baseCspProd (src/lib/security-headers.ts). Re-run tests.
Block (intentional)Browser extension, malicious injection, or removed feature.Do nothing — the block is correct. Optionally add a SQL filter to ignore the noise.
RefactorInline <script> or <style> we control.Move to an external file or a hashed inline. Avoid loosening script-src to 'unsafe-inline'.
Per-route overrideOnly one sensitive page needs a different policy.Add to securityOverrides in src/lib/security-headers.ts instead of widening the global CSP.

Common false positives to ignore

  • blocked_uri = 'about', 'data', 'inline', 'eval' from browser extensions (especially on Chrome with Grammarly, password managers, etc.).
  • Old client tabs that loaded a previous CSP version — the violation will stop after they refresh.
  • effective_directive = 'script-src-elem' from chrome-extension://... sources — always extension noise.

Filter them out of dashboards with:

AND blocked_uri NOT IN ('about', 'data', 'inline', 'eval')
AND blocked_uri NOT LIKE 'chrome-extension://%'
AND blocked_uri NOT LIKE 'moz-extension://%'
AND blocked_uri NOT LIKE 'safari-extension://%'

Operational hygiene

  • Retention: prune reports older than 90 days. Run monthly:
    DELETE FROM public.csp_reports WHERE received_at < now() - INTERVAL '90 days';
    
  • Volume guard: the endpoint rejects payloads > 32 KB and silently drops invalid JSON. If csp_reports row count spikes, check for an attacker POSTing junk and rate-limit at the edge.
  • Pre-deploy: after any change to baseCspProd, watch csp_reports.received_at > now() - INTERVAL '15 minutes' for ~30 min post-publish. A burst of new directives means the change broke something.

Testing the endpoint locally

curl -X POST http://localhost:3000/api/public/csp-report \
  -H 'Content-Type: application/csp-report' \
  -d '{"csp-report":{"document-uri":"http://localhost:3000/","violated-directive":"script-src","blocked-uri":"https://evil.example.com/x.js","effective-directive":"script-src","original-policy":"default-src self","disposition":"enforce"}}'

Expect HTTP 204. Then:

SELECT * FROM public.csp_reports ORDER BY received_at DESC LIMIT 1;