Documentation

Helix developer docs

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

Security Headers

Security response headers are set by securityHeaders middleware in src/start.ts via TanStack Start's request middleware. Values vary by environment, controlled by process.env.NODE_ENV.

Headers (always set)

HeaderValuePurpose
X-Frame-OptionsDENYBlock all framing (clickjacking protection).
X-Content-Type-OptionsnosniffPrevent MIME sniffing.
Referrer-Policystrict-origin-when-cross-originLimit referrer leakage.
Permissions-Policycamera=(), microphone=(), geolocation=()Disable unused browser APIs.
Content-Security-Policysee belowXSS / injection mitigation.

Production-only headers (NODE_ENV=production)

HeaderValuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadForce HTTPS for 1 year, preload-eligible.
Cross-Origin-Opener-Policysame-origin-allow-popupsProcess isolation against XS-Leaks while keeping Google/OAuth popups working.
Cross-Origin-Resource-Policysame-siteBlock cross-site embedding while still allowing our own subdomains.

HSTS is intentionally omitted in development so localhost over HTTP keeps working.

Cross-Origin headers — why these exact values

  • COOP same-origin-allow-popups (not same-origin): Supabase social sign-in opens an OAuth popup that calls window.opener.postMessage after the redirect. Strict same-origin severs that opener relationship and the popup can't deliver the auth result back. The relaxed variant keeps the cross-origin isolation guarantees we care about (XS-Leaks / Spectre) while preserving the popup channel.
  • CORP same-site (not same-origin): allows assets served from sibling subdomains (e.g. cdn.helixsecure.co.uk, *.lovable.app previews) to be embedded by the app. same-origin would block them outright.
  • COEP is intentionally NOT set. Enabling Cross-Origin-Embedder-Policy: require-corp would break Supabase Storage downloads, remote <img> sources, and Realtime websocket payloads, because those responses don't ship a matching Cross-Origin-Resource-Policy header. Only enable COEP if/when the app needs SharedArrayBuffer or high-resolution timers.

CORS

The app does not expose any public HTTP endpoints (no app-side CORS by default — but src/routes/api/public/csp-report.ts exists, and you may add more. Any new /api/public/* route MUST go through src/lib/cors.ts (withCors / corsPreflight) instead of writing Access-Control-* headers by hand.

The helper enforces:

  1. Access-Control-Allow-Origin: * cannot be combined with Access-Control-Allow-Credentials: true (throws at runtime).
  2. Origins must be bare (https://example.com, no path/trailing slash).
  3. Vary: Origin is set whenever the response varies by origin.

Two tests back this up:

  • src/lib/__tests__/cors.test.ts — unit tests for the helper.
  • src/routes/api/public/__tests__/no-wildcard-credentials.test.ts — static scan that fails the build if any /api/public/* file writes the unsafe combo or skips the helper.

Browser → Supabase REST/Realtime CORS is still handled by Supabase, and server functions (createServerFn) are same-origin RPC — no CORS needed.

Referrer-Policy

  • Global default: strict-origin-when-cross-origin — sends only the origin (no path/query) on cross-origin requests, so Supabase sees https://helixsecure.co.uk (needed for its CORS check) but never the internal URL or query string. Keeps full URL on same-origin.
  • Auth/credential routes (/login, /signup, /reset-password, /forgot-password, /verify-email): tightened to no-referrer so password-reset tokens and email-verification tokens in the URL never leak to any third party (analytics, fonts CDN, etc.).

CSP violation reporting

Production CSP includes both report-uri and report-to csp-endpoint, paired with a Reporting-Endpoints: csp-endpoint="/api/public/csp-report" header. Browsers POST violation reports to that endpoint; reports land in the csp_reports table.

See docs/csp-reports.md for the triage playbook, SQL queries, and retention guidance. Reporting is intentionally disabled in development.

Content-Security-Policy

Production CSP

default-src 'self';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com data:;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
script-src 'self' 'unsafe-inline';
connect-src 'self' https://*.supabase.co wss://*.supabase.co;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
  • 'unsafe-inline' on style-src is required by shadcn/Tailwind runtime styles.
  • 'unsafe-inline' on script-src is required by TanStack Start's hydration script. 'unsafe-eval' is not allowed in prod.
  • connect-src allows the Lovable Cloud (Supabase) REST + Realtime endpoints.
  • upgrade-insecure-requests auto-rewrites any accidental http:// subresource to https://.

Development CSP

Relaxed to make Vite HMR work:

  • script-src adds 'unsafe-eval' (React Refresh / Vite client).
  • connect-src adds ws:, wss:, and http://localhost:* (HMR socket).
  • img-src adds blob: (dev tooling previews).

Changing headers

Edit src/start.ts. The isProd branch toggles HSTS + COOP/CORP and swaps the CSP between cspProd and cspDev. Update this document when you change any header.

Verifying

curl -sI https://helixsecure.co.uk/ | grep -iE 'strict-transport|content-security|x-frame|referrer|permissions|cross-origin'

Or use https://securityheaders.com/. Target grade: A or higher in production.

Per-route overrides

Sensitive routes can tighten headers/CSP without affecting the rest of the app. Overrides live in securityOverrides in src/start.ts and match by pathname prefix (first match wins).

export const securityOverrides: SecurityOverride[] = [
  {
    prefix: "/reset-password",
    cspDirectives: {
      // REPLACES the global script-src for this route only
      "script-src": "'self'",
      "connect-src": "'self' https://*.supabase.co",
    },
    headers: {
      "cache-control": "no-store, no-cache, must-revalidate",
      "referrer-policy": "no-referrer",
    },
  },
];

Rules:

  • cspDirectives keys are CSP directive names (script-src, connect-src, …). The value REPLACES the directive for that route; other directives (e.g. frame-ancestors 'none') stay intact.
  • headers keys are lowercase header names. They override the defaults set by the middleware.
  • Order matters — put more specific prefixes (e.g. /admin/billing) before broader ones (e.g. /admin).

Default overrides shipped

Route prefixWhy
/login, /signup, /forgot-password, /verify-emailcache-control: no-store, referrer-policy: no-referrer to keep credentials/tokens out of caches and referrers.
/reset-passwordAbove + tighter CSP: script-src 'self' (no inline JS allowed), connect-src limited to backend only. The reset-token URL must never leak.

Add new overrides as you build sensitive pages (admin billing, key rotation, impersonation, etc.). Keep this table in sync.