# Content Security Policy

> The minimum CSP directives a partner site needs to host Hatched widgets, plus variations for custom domains and staging hosts.

Source: https://docs.hatched.live/docs/guides/widget-csp

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

```http
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](#self-hosting-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:

```http
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:

```http
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 style` — `style-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

```js
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:

```html
<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](/docs/reference/theme-tokens#typography) for
the full font-token list.
