Security Headers
Configure CSP, COOP, COEP, CORP, X-Frame-Options, HSTS, Referrer-Policy, and Permissions-Policy on every response from your generated Worker.
Every compiled Quickback Worker sets a baseline of security response-headers on every response: HSTS, X-Content-Type-Options: nosniff, a clickjacking CSP (frame-ancestors 'self' …TRUSTED_ORIGINS), X-Frame-Options: SAMEORIGIN, Referrer-Policy, and Permissions-Policy. Adding the security block lets you override individual headers, ship a fuller Content-Security-Policy, or suppress them all if an upstream gateway handles headers.
Default behavior
Omit security entirely → the baseline above. No config change needed for clickjacking + HSTS protection.
import { defineConfig } from '@quickback/compiler';
export default defineConfig({
name: 'my-api',
// ...
// security defaults are applied automatically.
});Suppress every security header
For Workers fronted by a gateway that owns headers:
export default defineConfig({
// ...
security: false,
});The compiler emits a no-op middleware comment so the route table still lines up with the rest of the generated code, but no c.header(...) calls fire.
Per-header overrides
Each field on the security block accepts either a typed value or false to suppress that specific header. Defaults still apply for fields you don't set.
export default defineConfig({
// ...
security: {
// Suppress just CSP — keeps HSTS, referrer, permissions, etc.
csp: false,
// Or override individual fields:
coop: 'same-origin',
coep: 'credentialless',
referrerPolicy: 'no-referrer',
permissionsPolicy: { geolocation: [], microphone: [], camera: [], payment: [] },
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
},
});Content Security Policy
security.csp accepts a typed object. Each *Src field is an array of source expressions. The compiler accepts both the @-prefixed sentinels (idiomatic, type-safe) and CSP wire-form keywords ("'self'", "'unsafe-inline'"); both normalize to the same output.
Sentinels
| Sentinel | Wire form | Notes |
|---|---|---|
@self | 'self' | Same-origin. |
@none | 'none' | Empty-set keyword. |
@unsafe-inline | 'unsafe-inline' | Allows inline scripts/styles — defeats most XSS mitigation when used in script-src. |
@unsafe-eval | 'unsafe-eval' | Allows eval() and friends. |
@trustedOrigins | The …TRUSTED_ORIGINS constant at runtime | Resolves to auth.trustedOrigins as configured in quickback.config.ts. |
@nonce | 'nonce-…' per request | Reserved for the per-request nonce middleware shipping in 0.10.15. Using this in 0.10.14 is a compile-time error. |
Unknown @… tokens are rejected at compile rather than silently stringified into the wire CSP.
Example: paste-in CSP for a Quickback SPA project
The bundled CMS and Account SPAs load Google Fonts cross-origin, so the strict 'self'-only CSP would white-screen them on load. This config is a known-working starting point for Quickback projects with the bundled SPAs:
export default defineConfig({
// ...
security: {
csp: {
defaultSrc: ['@self'],
// Tailwind dynamic styles + bundled Google Fonts CSS need
// unsafe-inline + the gstatic / googleapis hosts. Tightening
// styles to nonces ships in 0.10.15.
styleSrc: ['@self', '@unsafe-inline', 'https://fonts.googleapis.com'],
scriptSrc: ['@self'],
fontSrc: ['@self', 'data:', 'https://fonts.gstatic.com'],
imgSrc: ['@self', 'data:', 'blob:', 'https:'],
connectSrc: ['@self', '@trustedOrigins'],
frameAncestors: ['@self', '@trustedOrigins'],
formAction: ['@self'],
baseUri: ['@self'],
objectSrc: ['@none'],
upgradeInsecureRequests: true,
},
},
});Troubleshooting CSP violations
If the browser console shows CSP errors after enabling security.csp, match the directive named in the error against this table — it maps the failure mode back to the directive that needs widening.
| Console error | Directive | Why it happens | Fix |
|---|---|---|---|
Loading the stylesheet 'https://fonts.googleapis.com/...' violates 'style-src ...' | styleSrc | Bundled CMS/Account SPAs <link> to Google Fonts CSS. | Add 'https://fonts.googleapis.com' to styleSrc. |
Refused to load the font 'https://fonts.gstatic.com/...' because it violates 'font-src ...' | fontSrc | The CSS file from googleapis loads .woff2 files from gstatic. | Add 'https://fonts.gstatic.com' to fontSrc. |
Connecting to 'https://api.your-domain/...' violates 'connect-src 'self'' | connectSrc | API on a sibling subdomain (e.g. SPA on app.example.com, API on api.example.com) — 'self' only covers exact origin. | Use connectSrc: ['@self', '@trustedOrigins'] so the same allowlist cors() enforces gets reused. |
Refused to apply inline style because it violates 'style-src ...' | styleSrc | TanStack Router / Tailwind's style="..." attributes are inline. | Add '@unsafe-inline' to styleSrc until 0.10.15 ships per-request nonces. |
Refused to load the image '...' because it violates 'img-src ...' | imgSrc | User-uploaded avatars / R2-hosted assets / data: URIs from blob previews. | Widen imgSrc to ['@self', 'data:', 'blob:', 'https:'] (or list specific hosts). |
Refused to connect to 'wss://...' because it violates 'connect-src ...' | connectSrc | Realtime / Durable Object WebSockets. | The @trustedOrigins sentinel covers HTTPS only — add the explicit wss:// origin if your DO lives elsewhere. |
The general rule: 'self' means exact origin (scheme + host + port). Anything cross-origin needs an explicit host or the @trustedOrigins sentinel. The paste-in recipe above is a known-good baseline for a stock Quickback project — start there and tighten, rather than starting from 'self'-only and chasing console errors.
Auto-derivations
The compiler applies these so config stays minimal:
csp.frameAncestorsdefaults to['@self', '@trustedOrigins']whencspis set butframeAncestorsis omitted. Same allowlist as CORS.X-Frame-Optionsderives fromcsp.frameAncestors(['@none']→DENY, contains@self→SAMEORIGIN, otherwise omitted — XFO can't represent arbitrary origins so CSP becomes the truth there). Override withsecurity.frameOptions.corp: 'same-origin'is auto-emitted whencoepis set.require-corpwithout CORP breaks the ASSETS binding fetches on Cloudflare; the compiler couples them so users don't have to remember.
CSP auto-config (0.10.15+)
When you opt into a csp object, the compiler additively widens directives based on which Quickback features your project has enabled. Additive means anything you already wrote stays; we only push values that aren't already present.
| Trigger | Auto-added | To directive | Why |
|---|---|---|---|
cms: true OR account: true | 'unsafe-inline' | styleSrc | Tailwind / TanStack Router emit inline style= attributes |
cms: true OR account: true | https://fonts.googleapis.com | styleSrc | Bundled SPAs <link> to Google Fonts CSS |
cms: true OR account: true | https://fonts.gstatic.com | fontSrc | The googleapis CSS loads .woff2 from gstatic |
csp set (always) | @trustedOrigins | connectSrc | XHR/fetch must reach the same allowlist cors() permits — covers cross-subdomain APIs without re-listing them |
cms: true OR account: true | https://${domain} for each cms.domain / account.domain / domain / inferred unified domain | connectSrc | The Vite bundle hardcodes VITE_QUICKBACK_API_URL at compile time, so the SPA fetches that exact URL even when the page is served from a different origin (e.g. localhost dev hitting prod-baked SPA) |
Scaffold rule: if you set defaultSrc but omit the target directive (e.g. you wrote defaultSrc: ['@self'] and nothing for connectSrc), auto-config creates the new directive seeded with your defaultSrc values plus the addition. defaultSrc itself stays clean. So defaultSrc: ['@self'] + auto-config produces:
default-src 'self'
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com
font-src 'self' https://fonts.gstatic.com
connect-src 'self' @trustedOrigins https://api.your-app.comEach directive is scoped to exactly what it needs. defaultSrc is not widened with style-only or connect-only hosts — that would let those hosts also reach script-src / img-src / media-src via fall-back, which is wider than the bundled SPA actually needs. (This rule changed in 0.10.17 — earlier versions incorrectly pushed additions into defaultSrc.)
Visibility: every applied addition prints to stderr at compile time — one line per addition with the directive, value, and reason. No silent magic. Loop's compile output now looks like:
[security] auto-added 'unsafe-inline' to csp.styleSrc — account: true → bundled Account SPA → inline style attributes from Tailwind / TanStack Router
[security] auto-added https://fonts.googleapis.com to csp.styleSrc — account: true → bundled Account SPA → loads Google Fonts CSS cross-origin
[security] auto-added https://fonts.gstatic.com to csp.fontSrc — account: true → bundled Account SPA → woff2 font files served from gstatic.com
[security] auto-added @trustedOrigins to csp.connectSrc — CORS-trusted origins must be reachable from XHR/fetch (matches cors() allowlist)Opt-out: security: { cspAutoConfig: false, csp: { ... } } for projects fronted by a CDN that injects its own CSP, or teams that want strict from-scratch control. With auto-config off, every value above must be authored by hand.
With auto-config on, the paste-in recipe above becomes much shorter — most users only need:
security: {
csp: {
defaultSrc: ['@self'],
scriptSrc: ['@self'],
imgSrc: ['@self', 'data:', 'blob:', 'https:'],
objectSrc: ['@none'],
upgradeInsecureRequests: true,
},
}The compiler fills in the SPA-specific font hosts, inline-style allowance, and trusted-origin connect rules from the project's own feature flags.
Cross-origin window/embedder isolation
security: {
coop: 'same-origin', // Defends against window.opener back-references from popups.
coep: 'credentialless', // Required for SharedArrayBuffer / cross-origin isolation.
// corp auto-emits as 'same-origin' here.
}HSTS
Default: max-age=31536000; includeSubDomains; preload (HSTS preload-list eligible).
security: {
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
}The generated middleware automatically suppresses HSTS on localhost, 127.0.0.1, and *.localhost regardless of this config. Without that gate, wrangler dev would pin the developer's browser to HTTPS for maxAge seconds, which is observable across every project on that machine.
To submit to the HSTS preload list, you need:
maxAge≥ 31536000 (one year)includeSubDomains: truepreload: true
The compiler warns at compile time if preload: true without the other two — the preload list rejects either anyway.
Permissions Policy
Map of feature name → list of allowed origins.
security: {
permissionsPolicy: {
geolocation: [], // disabled for everyone
microphone: ['self'], // allowed only on own origin
camera: ['self', 'https://meet.example.com'],
payment: [],
},
}The compiler encodes the wire syntax (which differs from CSP — values use (), not space-separated tokens, with bareword self and quoted URLs).
Compile-time lint rules
The compiler walks the resolved config and emits warnings (or errors) for known footguns. Errors block compile; warnings print to stderr.
| Code | Severity | Trigger |
|---|---|---|
csp/nonce-not-yet-supported | error | @nonce sentinel used in 0.10.14. Per-request nonce middleware ships in 0.10.15. |
csp/unsafe-inline-with-nonce | error | @unsafe-inline and @nonce in the same directive (CSP3 silently ignores unsafe-inline when nonce is present). |
csp/empty-resolved-directive | error | *Src array contains only @trustedOrigins and auth.trustedOrigins is empty — would emit a directive Chrome reads as 'none'. |
csp/unsafe-inline-script | warn | @unsafe-inline in scriptSrc defeats most XSS mitigations. |
csp/open-source | warn | Wildcard * or scheme-only https: / http: in any *-src. |
csp/unsafe-eval-with-data | warn | @unsafe-eval combined with any data: source — trivial bypass. |
csp/missing-default-src | warn | Other *-src set without defaultSrc. |
csp/upgrade-with-http | warn | upgradeInsecureRequests: true plus an http:// source elsewhere. |
csp/report-uri-only | warn | reportUri set without reportTo (deprecated CSP2 path). |
hsts/preload-needs-long-max-age | warn | preload: true with maxAge < 31536000. |
hsts/preload-needs-include-subdomains | warn | preload: true without includeSubDomains. |
Verification
Spin up the deployed Worker (or wrangler dev) and check the response headers:
curl -sI https://my-api.example.com/health \
| grep -E '^(content-security-policy|x-frame-options|strict-transport-security|cross-origin-opener-policy|cross-origin-embedder-policy|cross-origin-resource-policy|referrer-policy|permissions-policy):'Use securityheaders.com for an external grade — Quickback's defaults are A on the baseline and A+ once a fuller csp is configured.
AI Agent Readiness
Every Quickback Worker ships agent-discovery files and an auto-generated MCP server. Let Claude, ChatGPT, Cursor, and Perplexity find and call your API out of the box.
Auth & JWT
App-level auth settings — role hierarchy and JWT minting / verification configuration for the bearer-token fast-path.