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.
<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
widgetThe 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
sidebarAn 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
embedAn 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.
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.
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:
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 bothscript-srcandframe-src.fonts.googleapis.comandfonts.gstatic.com: the Google Fonts stylesheet and font files.'unsafe-inline'instyle-srccovers the injected styles.wss://<your-supabase-ref>.supabase.co: the Supabase realtime socket. Fill in the ref from your ownNEXT_PUBLIC_SUPABASE_URL.<your-sentry-ingest-host>: the Sentry ingest endpoint, derived from yourNEXT_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.Under
strict-dynamic, the host allowlist inscript-srcis ignored. Only scripts trusted by a nonce or hash run. Soscript-src https://widget.corveria.comdoes nothing for you; the loader must carry a nonce to run at all. Once it does,strict-dynamicpropagates that trust to theanna-widget.jsscript the loader injects.2.The loader forwards its nonce. At execution it reads
document.currentScript.nonceand applies it to the script (widget) and the style (sidebar) it injects. On a plain nonce-CSP withoutstrict-dynamic, those injections are accepted via the forwarded nonce, so you need neitherscript-src <origin>norstyle-src 'unsafe-inline'for the loader's own injections.
# 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.
| Framework | How the loader is placed | Nonce carried? | Notes |
|---|---|---|---|
| Plain HTML | The documented script tag, placed directly | n/a | Works as-is. Add host entries only if you ship a non-nonce CSP. |
| Next.js (nonce / strict-dynamic) | next/script with the request nonce | Yes, forwarded | The 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-CSP | Injected at runtime, nonce set manually | Yes, if you set the nonce before append | Vite 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 tag | n/a | Use the host-allowlist directives from the CSP section verbatim. |
Next.js: next/script with a nonce
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
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:
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