HatchedDocs
Guides

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.com and font-src https://fonts.gstatic.com — widgets @import their 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-src is missing or doesn't include the API host.
  • Refused to load script 'https://cdn.hatched.live/widget.js'script-src is missing the CDN host.
  • Refused to apply inline stylestyle-src lacks 'unsafe-inline'; widgets render unstyled.
  • Refused to load image 'https://cdn.hatched.live/...'img-src is 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.