Content Security Policy
The minimum CSP directives a partner site needs to host Hatched widgets, plus variations for custom domains and staging hosts.
Hatched widgets load via a <script> tag, talk to api.hatched.live,
mount inside a closed Shadow DOM, and inject styles into that root. A
partner site with a strict Content Security Policy must allow each of
those operations or the widget will silently fail.
This page lists the minimum directives needed, then walks through the common variations: custom domains and dev/staging hosts.
Minimum policy
Content-Security-Policy:
script-src 'self' https://cdn.hatched.live;
connect-src 'self' https://api.hatched.live;
img-src 'self' https://cdn.hatched.live data:;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com;Why each line:
script-src https://cdn.hatched.live— the loader bundle lives on this host. Per-widget chunks are dynamically imported from sibling URLs on the same origin.connect-src https://api.hatched.live— every API call (/widget/state,/widget/theme,/widget/track, marketplace, kudos, etc.) leaves the browser bound for this host.img-src https://cdn.hatched.live data:— buddy art, badge icons, marketplace items, and stage emblems are CDN-served.data:covers inline SVG placeholders the widget renders while images load.style-src 'unsafe-inline'— the loader injects a<style>tag into each widget's Shadow DOM so partner styles cannot leak across the boundary in either direction. Shadow-scoped inline styles cannot be delivered via an external stylesheet, and the injected<style>elements carry no nonce, so'unsafe-inline'is required for widget styling.style-src https://fonts.googleapis.comandfont-src https://fonts.gstatic.com— widgets@importtheir display fonts (Gluten, Geist Mono, Instrument Serif) from Google Fonts. Without these two hosts the import is blocked and widgets fall back to system fonts. You can drop both lines if you self-host the fonts by overriding the--hw-font-*tokens.font-src data:— widget themes may also inline display fonts as data-URLs (display-condensed personality, for example).
No Hatched widget embeds an <iframe> at runtime, so no frame-src
directive is required to host one.
Why no unsafe-eval
Hatched bundles ship as pre-compiled JavaScript — no runtime eval,
new Function, or templated module imports. You never need unsafe-eval
to host a Hatched widget.
Custom domains
If you proxy cdn.hatched.live behind your own subdomain (recommended for
brand-conscious deployments), update the directives accordingly:
Content-Security-Policy:
script-src 'self' https://widgets.example.com;
connect-src 'self' https://api.hatched.live;
img-src 'self' https://widgets.example.com data:;
…You still hit api.hatched.live directly — only the widget JS travels
through the proxy.
Staging / dev hosts
For local development with a staging tenant:
Content-Security-Policy:
script-src 'self' http://localhost:* https://cdn.hatched.live https://cdn.staging.hatched.live;
connect-src 'self' http://localhost:* https://api.staging.hatched.live;
…Production policies must drop the http://localhost:* and staging
hosts (cdn.staging.hatched.live, api.staging.hatched.live) — leaving
them allows downgrade attacks. Use Next.js / Express
middleware to render different policies per environment if you build a
single binary.
Debugging a CSP rejection
The browser console reports CSP violations with the directive that blocked the request. Common signals:
Refused to connect to 'https://api.hatched.live/...'—connect-srcis missing or doesn't include the API host.Refused to load script 'https://cdn.hatched.live/widget.js'—script-srcis missing the CDN host.Refused to apply inline style—style-srclacks'unsafe-inline'; widgets render unstyled.Refused to load image 'https://cdn.hatched.live/...'—img-srcis missing the CDN host.
If you use a CSP reporting endpoint, every violation surfaces there with
the same effective-directive payload. Add Hatched directives to the
allowlist before going live and watch the report stream for the first
hour after release.
A complete next.config.mjs example
const csp = [
"script-src 'self' https://cdn.hatched.live",
"connect-src 'self' https://api.hatched.live",
"img-src 'self' https://cdn.hatched.live data:",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' data: https://fonts.gstatic.com",
].join('; ');
export default {
async headers() {
return [
{
source: '/(.*)',
headers: [{ key: 'Content-Security-Policy', value: csp }],
},
];
},
};That policy ships every Hatched widget end-to-end without further tuning.
Self-hosting fonts
The Google Fonts hosts in style-src / font-src exist only because
widgets @import their display fonts from Google. If your policy forbids
third-party font hosts — or you want to drop the render-blocking external
request — override the --hw-font-* tokens to point at fonts you already
serve, then remove https://fonts.googleapis.com and
https://fonts.gstatic.com from the policy:
<script
src="https://cdn.hatched.live/widget.js"
data-session-token="{{session_token}}"
data-theme-vars='{"--hw-font-display":"\"Brand Display\", system-ui, sans-serif","--hw-font-body":"\"Brand Sans\", system-ui, sans-serif","--hw-font-mono":"\"Brand Mono\", ui-monospace, monospace"}'
></script>The loader still emits the Google Fonts @import (it is harmless when the
overridden families resolve first), but if the import is CSP-blocked the
widgets simply use your --hw-font-* families with no visual regression.
See Theme tokens → Typography for
the full font-token list.
Verify webhooks end-to-end
Copy-paste handlers that capture the raw body, verify the HMAC signature, and acknowledge fast — for Express, Fastify, Hono, Next.js App Router, Next.js Pages Router, and Cloudflare Workers.
Unlock gates
Spend primary tokens to unlock features — the non-cosmetic half of the token economy.