Skip to content

Security Headers

This app is deployed on Cloudflare Pages. Security headers and cache rules are applied at the edge via public/_headers. SPA client-side routing is supported via public/_redirects (fallback to /index.html with HTTP 200).

These headers are only active in production builds served through Cloudflare Pages. The local dev server (npm run dev) does NOT apply them — test header behavior in a preview/staging deploy.

Headers applied to every route (/*)

  • X-Frame-Options: DENY — legacy browser protection against clickjacking. The modern equivalent is frame-ancestors 'none' in the CSP; we ship both.
  • X-Content-Type-Options: nosniff — stops browsers from MIME-sniffing responses and running, for example, a text file as a script.
  • Referrer-Policy: strict-origin-when-cross-origin — sends only the origin (not the full URL with query/path) on cross-origin requests. Prevents leaking booking IDs, tokens in query params, etc.
  • Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() — disables browser APIs we do not use and opts out of FLoC/Topics.
  • Strict-Transport-Security: max-age=63072000; includeSubDomains; preload — forces HTTPS for two years and declares intent to join the HSTS preload list. Cloudflare already terminates TLS; this is defense in depth.
  • X-DNS-Prefetch-Control: off — prevents the browser from pre-resolving links on a page (reduces passive fingerprinting surface).
  • Content-Security-Policy — see below.

Content-Security-Policy allowlist

The CSP whitelists exactly the third parties the app talks to at runtime:

Directive Allowed origins Reason
default-src 'self' Everything falls back to same-origin unless overridden.
script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://accounts.google.com https://apis.google.com https://connect.facebook.net App bundles are same-origin. 'unsafe-inline' is required by the Facebook Pixel snippet injected in index.html (public site only). Turnstile, Google Identity Services, and Facebook Pixel each load a script tag from their respective hosts. The prod bundle was grepped for eval( / new Function( — no hits — so 'unsafe-eval' is intentionally NOT granted.
connect-src 'self' https://*.supabase.co wss://*.supabase.co https://www.googleapis.com https://accounts.google.com https://api.exchangerate-api.com https://www.facebook.com https://connect.facebook.net Supabase REST + realtime WebSocket; Google Drive REST; Google OAuth; exchange-rate lookup in admin currency settings; Facebook Pixel beacons.
img-src 'self' data: https: blob: Permissive by design: Supabase Storage public URLs, Google Drive thumbnails, data/blob URLs from PDF/file previews, and the Facebook tr? tracking pixel all need to load.
style-src 'self' 'unsafe-inline' Tailwind/shadcn inject runtime inline styles (component-level style={...} attributes). Tightening this later would require a nonce strategy.
font-src 'self' data: All fonts are self-hosted in /public/fonts/. data: covers any inlined glyph payloads.
frame-src https://challenges.cloudflare.com https://accounts.google.com Turnstile challenge iframe and the Google OAuth popup iframe.
object-src 'none' Block Flash/plugins.
base-uri 'self' Stops injected <base> tags from redirecting relative URLs.
form-action 'self' Forms can only submit back to our origin.
frame-ancestors 'none' Nobody may embed this app in an iframe.
upgrade-insecure-requests Rewrites any stray http:// resource to https://.

Cache rules

  • /assets/* and /fonts/*public, max-age=31536000, immutable. Vite emits content-hashed filenames, so these are safe to cache for a year.
  • /index.htmlpublic, max-age=0, must-revalidate. Ensures every deploy is picked up on the next navigation; hashed assets it references cache fine.
  • Everything else inherits Cloudflare Pages defaults.

Adding a new third-party script

If you integrate a new external SDK (analytics, chat widget, payments...):

  1. Identify the script host (the URL of the <script src>). Add it to script-src.
  2. Identify the API hosts the SDK calls (XHR / fetch / WebSocket). Add them to connect-src. Include wss:// variants for WebSockets.
  3. If the SDK opens an iframe, add its host to frame-src.
  4. If it loads fonts from a CDN, add to font-src.
  5. Deploy to a staging/preview branch first. Open devtools and watch for Refused to load ... because it violates the following Content Security Policy messages. Fix any directive that is too strict, then promote to prod.

Testing before prod rollout

  • Deploy the demo (or any preview) branch to Cloudflare Pages.
  • Open the preview URL, load the public site, log in as customer, partner, and admin, and exercise Google Drive upload + Turnstile widget.
  • In browser devtools, Console tab, filter for "Content Security Policy" — there should be zero violations.
  • Network tab: verify Strict-Transport-Security, X-Frame-Options, and Content-Security-Policy headers are present on the HTML document response.
  • Verify /assets/<hash>.js responses carry cache-control: public, max-age=31536000, immutable.