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 isframe-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.html—public, 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...):
- Identify the script host (the URL of the
<script src>). Add it toscript-src. - Identify the API hosts the SDK calls (XHR / fetch / WebSocket). Add them to
connect-src. Includewss://variants for WebSockets. - If the SDK opens an iframe, add its host to
frame-src. - If it loads fonts from a CDN, add to
font-src. - Deploy to a staging/preview branch first. Open devtools and watch for
Refused to load ... because it violates the following Content Security Policymessages. 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, andContent-Security-Policyheaders are present on the HTML document response. - Verify
/assets/<hash>.jsresponses carrycache-control: public, max-age=31536000, immutable.