Developers

Adding Anna to your website

For most websites the install is a single line, and Anna is live. This page starts with that line. Everything after it, the Content-Security-Policy directives, nonces, and framework specifics, only applies if your site runs a strict CSP (for example a custom Next.js app). The goal: set up Anna without decompiling the loader, whether your site is simple or locked down.

Basic install

One line. You add a script tag and set data-client to the client's slug. You do not pick the mode yourself, the client's server-side config does (widget, sidebar, or embed). Place the tag exactly once per page.

HTML
<script
  async
  src="https://widget.corveria.com/v1/loader.js"
  data-client="your-client-slug"
></script>

Done for most sites

That's all it takes for most websites. If your site runs no Content-Security-Policy, which covers most WordPress, Shopify, Wix, and plain sites, you're done. Read on below only if your site has a strict CSP.

The script tag attributes

data-client
Required. The client's slug. Without it the loader logs a console error and does nothing else.
data-api
Optional. Overrides the platform base URL. Defaults to the origin derived from the script's own src, which is right in nearly every case.

There is no data-mode attribute and no ?mode= query param. The mode is decided server-side by the client's config.

The three load modes

One script tag renders three very different surfaces. Which one you get is driven by the client's server-side config, not by your page.

Widget

widget

The classic bubble in the corner. Runs directly in your page via Shadow DOM, not in an iframe. This mode puts the most on your CSP, because everything the widget uses runs in your page's context.

Sidebar

sidebar

An iframe docked to the left or right edge of the viewport that pushes page content aside when it opens. All of its content is encapsulated in the iframe on the platform origin.

Embed

embed

An iframe inside a container you provide, `#annasvarar-embed` by default, at least 220x400 px. Fills the container. Like the sidebar, everything is encapsulated in the iframe.

Because the mode is server-driven, confirm it with the Anna admin before you build your CSP rule: it decides which directives you need.

Allowed domains

Before Anna's chat loads on your site, your domain must be on the client's allowed-domains list. It governs which sites may fetch the client's config and frame the chat. Add the domain in the dashboard (Settings, allowed domains) or have the Anna admin do it. Subdomains can be allowed with *.example.com.

Local testing

To test on your own machine before DNS points at the live site, add localhost to the allowed domains. The check looks at the hostname only and ignores the port, so a single localhost entry covers every port (3000, 5173, and others). Removing it before launch is good hygiene.

CSP directives by mode

If your site ships a Content-Security-Policy, the directives Anna needs depend on the load mode, and the difference is large. Sites without a CSP need none of this.

Do not confuse two policies. frame-ancestors sits on the platform's response for /sidebar/<slug> and controls who may frame Anna's chat; the platform sets it from the allowed domains, and you add nothing on your side. What follows is your own CSP: what your site's policy must permit so the loader (and, in widget mode, everything it injects) is not blocked.

All modes: the loader itself

The loader is fetched as a script and fetches its config over the network. These two directives are always required.

CSP
script-src  https://widget.corveria.com;
connect-src https://widget.corveria.com;

Sidebar and embed: allow the frame

You also need to allow the frame that renders the chat. That is the whole addition: Turnstile, fonts, the Supabase socket, and Sentry all live inside the platform iframe and are governed by the platform's CSP, not yours.

CSP
frame-src https://widget.corveria.com;

frame-src cannot be nonced. Nonces and strict-dynamic apply to <script> and <style> only, not frames, so sidebar and embed always need frame-src with the platform origin. A common hardened default is frame-src 'none', which lets the launcher button render and the config fetch succeed but never opens the panel. The loader now names this in the console.

In-page widget: allow everything it touches

In widget mode the loader injects `anna-widget.js` directly into your page, so Turnstile, Google Fonts, the Supabase socket, and Sentry all run in your CSP context. You have to allow them all:

CSP
script-src  https://challenges.cloudflare.com;
frame-src   https://challenges.cloudflare.com;
style-src   https://fonts.googleapis.com 'unsafe-inline';
font-src    https://fonts.gstatic.com;
connect-src wss://<your-supabase-ref>.supabase.co <your-sentry-ingest-host>;
  • challenges.cloudflare.com: Cloudflare Turnstile (bot protection) loads a script and renders a challenge iframe, hence both script-src and frame-src.
  • fonts.googleapis.com and fonts.gstatic.com: the Google Fonts stylesheet and font files. 'unsafe-inline' in style-src covers the injected styles.
  • wss://<your-supabase-ref>.supabase.co: the Supabase realtime socket. Fill in the ref from your own NEXT_PUBLIC_SUPABASE_URL.
  • <your-sentry-ingest-host>: the Sentry ingest endpoint, derived from your NEXT_PUBLIC_SENTRY_DSN. Omit if you do not run Sentry.

Both placeholders above are deployment-specific. Read them from your own NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SENTRY_DSN rather than copying a literal value.

Recommendation

Sites with a strict CSP should prefer sidebar or embed mode. Those modes encapsulate Turnstile, fonts, the Supabase socket, and Sentry inside the platform iframe, so your policy only adds script-src (or a nonce), connect-src, and frame-src for the platform origin. Widget mode pushes that whole third-party surface into your page's CSP. The mode is server-driven, so coordinate the choice with the Anna admin when the client is set up.

Nonce and strict-dynamic (Next.js)

This is where most CSP integrations get stuck. The raw `<script async>` loader tag is the one form that does not carry a nonce, and under a nonce-CSP an unnonced script simply does not run, with no error the browser attributes to Anna, just a dead widget.

  1. 1.Under strict-dynamic, the host allowlist in script-src is ignored. Only scripts trusted by a nonce or hash run. So script-src https://widget.corveria.com does nothing for you; the loader must carry a nonce to run at all. Once it does, strict-dynamic propagates that trust to the anna-widget.js script the loader injects.

  2. 2.The loader forwards its nonce. At execution it reads document.currentScript.nonce and applies it to the script (widget) and the style (sidebar) it injects. On a plain nonce-CSP without strict-dynamic, those injections are accepted via the forwarded nonce, so you need neither script-src <origin> nor style-src 'unsafe-inline' for the loader's own injections.

CSP
# All modes: nonce the loader so it runs under strict-dynamic.
script-src  'nonce-<per-request>' 'strict-dynamic';
connect-src https://widget.corveria.com;

# Sidebar / embed: add the frame (never nonce-able).
frame-src https://widget.corveria.com;

A nonce does not help the frame. frame-src has no nonce mechanism, so sidebar and embed always need frame-src <origin> regardless of any nonce. It also does not cover the runtime styles of anna-widget.js in widget mode, which is why that mode still wants 'unsafe-inline' styles.

Placing the loader by framework

The nonce is invisible in plain HTML but load-bearing in a Next.js nonce-CSP. Where the <script> tag comes from decides whether it carries one.

Placing the loader by framework
FrameworkHow the loader is placedNonce carried?Notes
Plain HTMLThe documented script tag, placed directlyn/aWorks as-is. Add host entries only if you ship a non-nonce CSP.
Next.js (nonce / strict-dynamic)next/script with the request nonceYes, forwardedThe intended path. A raw script async tag in a nonce-CSP page will not carry the nonce and gets blocked. The most common failure.
Vite / SPA with a nonce-CSPInjected at runtime, nonce set manuallyYes, if you set the nonce before appendVite has no built-in nonce plumbing; supply the nonce from wherever your server exposes it.
Any framework, host-allowlist CSP (no nonce)The raw script async tagn/aUse the host-allowlist directives from the CSP section verbatim.

Next.js: next/script with a nonce

app/layout.tsx
import Script from "next/script";

<Script
  src="https://widget.corveria.com/v1/loader.js"
  data-client="your-client-slug"
  nonce={nonce}
  strategy="afterInteractive"
/>

Vite / SPA: inject at runtime

JavaScript
const s = document.createElement("script");
s.src = "https://widget.corveria.com/v1/loader.js";
s.async = true;
s.dataset.client = "your-client-slug";
s.nonce = window.__CSP_NONCE__;
document.head.appendChild(s);

Diagnostics that tell you what is missing

Because the loader runs inside your document, it sees your CSP's securitypolicyviolation events. When a blocked URI belongs to the platform origin, it logs an actionable console error naming the exact directive to add. For example:

console
Anna: your site's Content-Security-Policy blocked
https://widget.corveria.com/sidebar/acme (frame-src).
Add to your CSP: frame-src https://widget.corveria.com.
See Anna's widget CSP integration guide for the full per-mode list.

The messages cover frame-src, script-src, style-src, connect-src, font-src, and img-src, and each directive is reported once. They cannot report a block of loader.js itself: if your CSP blocks the loader, this code never runs and you see the browser's own violation line for the loader URL. That means the loader script is not nonced or host-allowed.

Common problems

Need a hand with the setup?

If you are embedding Anna's chat under a strict CSP and something is blocking it, get in touch. Tell us your framework and the mode, and we will send you the exact directives.

Get in touch