Content-Security-Policy is an HTTP response header that tells the browser exactly which resources are allowed to load on your page. It is the single most effective defense against cross-site scripting (XSS) attacks, which remain the most common web application vulnerability. For a SaaS CRM like SalesSheet that handles sensitive customer data — emails, phone numbers, deal values, conversation histories — XSS prevention is not optional. A single successful injection could exfiltrate an entire contact database.
The way CSP works is elegant in its simplicity. You declare a policy that specifies allowed sources for scripts, styles, images, fonts, API connections, and other resource types. The browser enforces the policy and blocks anything that does not match. If an attacker manages to inject a <script> tag into your page via a stored XSS vulnerability, CSP blocks the script from executing because it did not come from an approved source. No execution, no data theft, no damage.
We implemented CSP as part of our production hardening checklist alongside rate limiting, server-side API key management, and input sanitization. CSP is the last line of defense — it catches what your other security layers miss. Here is exactly how we configured it for a real SaaS application running on Supabase and Vercel.
SalesSheet runs as a Remix application deployed on Vercel, with Supabase as the backend, Stripe for payments, PostHog for analytics, and Telnyx for telephony. Each of these services requires specific domains in our CSP allowlist. Here are the key directives and the reasoning behind each one:
The biggest CSP headache for any modern React application is inline styles. Frameworks like Emotion, styled-components, and even Tailwind's JIT compiler inject <style> tags dynamically. A strict CSP that blocks 'unsafe-inline' for styles will break your entire application's visual presentation on the first page load.
The textbook solution is nonce-based styles: generate a cryptographic nonce on each request, add it to your CSP header as style-src 'nonce-abc123', and inject that same nonce into every <style> tag. This is straightforward in server-rendered applications where you control the HTML output. In a Remix application with client-side hydration, it gets complicated. Every dynamically created style tag during React hydration also needs the nonce, which means threading it through your entire component tree.
We evaluated three approaches: nonce-based styles with a Remix entry.server.tsx modification, a hash-based approach where we pre-compute SHA-256 hashes of known inline styles, and simply allowing 'unsafe-inline' for styles while keeping scripts locked down. We chose the third option. The security risk of inline styles is orders of magnitude lower than inline scripts. An attacker who can inject CSS can create visual deception (phishing overlays), but cannot exfiltrate data or execute arbitrary code. Given that our script-src policy is strict and does not allow 'unsafe-inline', the overall security posture remains strong.
CSP is about pragmatic security, not perfection. A strict script-src with a permissive style-src is vastly better than no CSP at all. Ship the 90% solution and iterate.
The first deployment with CSP enabled produced a wall of console violations. Every browser extension our team had installed tried to inject scripts and got blocked — password managers, ad blockers, developer tools. These are expected and harmless. The violations show up in the browser console as warnings, but users see no impact because extension scripts are not critical to application function.
The real issues were subtler. PostHog's toolbar feature (used for session recording setup) injects an inline script that our CSP blocked. We had to add PostHog's CDN domain to script-src and update our PostHog initialization to use their external script loader rather than inline snippet. Stripe Elements worked immediately because Stripe designed their SDK with strict CSP in mind — everything loads from js.stripe.com via an iframe. Supabase's realtime client needed both wss:// and https:// entries in connect-src because it uses WebSockets for subscriptions and HTTPS for REST operations.
One surprising breakage was our Telnyx calling integration. The WebRTC SDK establishes WebSocket connections to Telnyx's signaling servers, and the specific domain pattern was not documented in their CSP guide. We had to inspect network requests during a test call to identify that wss://*.telnyx.com was the correct pattern. Without connect-src allowing this, calls would silently fail to connect — no error in the UI, just a call that never rings.
CSP supports a report-uri directive (and the newer report-to header) that tells the browser to send a JSON report to a specified endpoint whenever a violation occurs. We set up a simple Supabase Edge Function that receives these reports and logs them to a dedicated table. This gives us visibility into what is getting blocked in production across all users, not just our own testing browsers.
The iterative approach is important. We started with a more permissive policy — broader wildcards in connect-src, for example — and tightened it over two weeks as we confirmed which specific domains were actually needed. The violation reports told us exactly what to keep and what to remove. If a domain showed zero violations after two weeks, it was safe to drop from the allowlist. If a new feature introduced a new external dependency, the violation report caught it before users noticed.
We also use the Content-Security-Policy-Report-Only header in staging environments. This header logs violations without actually blocking anything, which lets us test new CSP rules against real application behavior before enforcing them in production. Deploy to staging with Report-Only, check the logs for unexpected violations, fix configuration, then promote to enforcing mode in production.
The most common CSP mistake is using 'unsafe-inline' and 'unsafe-eval' in script-src. These two directives effectively disable CSP for scripts — if inline scripts are allowed, an XSS payload runs without restriction. If eval is allowed, any string can become executable code. We see SaaS applications ship CSP headers with these directives and call it security. It is theater. Your script-src must not include 'unsafe-inline' or 'unsafe-eval' for CSP to provide meaningful XSS protection.
The second mistake is overly broad wildcards. A connect-src of https://* allows your application to make API calls to any HTTPS endpoint, which means an XSS attack can exfiltrate data to any server. Be specific: list exact domains, use subdomain wildcards only when necessary (like https://*.supabase.co because Supabase project URLs are subdomains), and audit the list regularly.
The third mistake is setting CSP once and forgetting it. Every new integration, every new analytics tool, every new font or CDN changes your resource loading pattern. CSP must be a living configuration that evolves with your application. We review our CSP headers as part of every deployment that adds a new third-party dependency. The enterprise-grade security our customers expect requires this ongoing discipline.
On Vercel, CSP headers are configured in the vercel.json file under the headers array, or in Remix's entry.server.tsx for dynamic header generation. We use the vercel.json approach for the CSP header itself because it applies to all routes consistently without requiring server-side rendering logic. The header is a single long string that Vercel injects into every response.
Supabase Edge Functions that serve API responses get their own CSP considerations. Functions that return HTML (like our email template previews) include a restrictive CSP that blocks all external resources. Functions that return JSON do not need CSP headers because JSON responses are not rendered as HTML by the browser. However, we do set X-Content-Type-Options: nosniff on all responses to prevent MIME type confusion attacks.
The combination of Vercel's edge network, Supabase's row-level security, and a tight CSP policy gives SalesSheet a security posture that matches or exceeds most enterprise CRM platforms — at a fraction of the cost and complexity. Security should not be a premium feature. It is a baseline expectation.