# Implementing Snakkbar in a site — guide for AI agents

You are an agent with control over a website's code and want to add Snakkbar to it. Follow these
steps in order. Every step ends with a check; do not move on until it passes.

## 0. Gather inputs

Ask the human for anything you cannot find yourself:

| Input | Example | Where it comes from |
| --- | --- | --- |
| `SNAKKBAR_HOST` | `https://www.snakkbar.app` | Snakkbar |
| `TENANT_ID` | `amatec` | Snakkbar (admin → Innstillinger) |
| `SNAKKBAR_SECRET_KEY` | `sk_snakkbar_…` | Only needed for steps 3 and 4. Admin → Innstillinger → API-nøkler. Store as a server secret. |
| Production origin(s) | `https://www.example.no`, `https://example.no` | The site you control |

Check: `curl $SNAKKBAR_HOST/api/v1/tenants/$TENANT_ID/config` returns JSON with the assistant's name.

Then decide, per side, how deep to integrate:

- **Visitor side**: stock widget (default — choose this unless the human wants a custom chat
  design) or own UI with the SDK.
- **Staff side**: hosted panel at `$SNAKKBAR_HOST/panel` (nothing to build), stock admin embedded
  in the site's own back office (step 3), or own UI with the SDK (step 5).

## 1. Allow the site's origins

The widget and embedded admin only work on allowed origins. As an admin in the Snakkbar admin
(Innstillinger → *Nettsteder som kan vise chatten*) add every origin the site is served from. If
you have an admin operator token, you can do it via the API:

```bash
curl -X PATCH "$SNAKKBAR_HOST/api/v1/operator/tenant" \
  -H "authorization: Bearer $OPERATOR_TOKEN" -H "content-type: application/json" \
  -d '{"allowedOrigins":["https://www.example.no","https://example.no"],"siteHosts":["www.example.no","example.no"]}'
```

`siteHosts` are the hosts the assistant may read for answers (and where staff may send visitors).

Check: `GET /operator/tenant` lists the origins. (Local development on `http://localhost:*` works
without this.)

## 2. Add the visitor widget

Add once, on every page, just before `</body>`:

```html
<script src="https://<SNAKKBAR_HOST>/widget.js" data-tenant="<TENANT_ID>" defer></script>
```

Framework recipes:

- **Next.js (App Router)** — in `app/layout.tsx`:
  ```tsx
  import Script from "next/script";
  // inside <body>:
  <Script src={`${process.env.NEXT_PUBLIC_SNAKKBAR_HOST}/widget.js`} data-tenant="<TENANT_ID>" strategy="afterInteractive" />
  ```
- **React SPA (Vite etc.)** — put the tag in `index.html`.
- **Astro / SvelteKit / Nuxt** — put the tag in the root layout's HTML.
- **WordPress** — theme footer (`footer.php`) or a "header & footer scripts" plugin.
- **Content Security Policy** — see [widget.md](widget.md#content-security-policy).

Options and the JavaScript API (`Snakkbar.open()`, events, identified visitors) are in
[widget.md](widget.md). Add a "Chat med oss" link anywhere with `onclick="Snakkbar.open()"`.

**Live visitors and consent.** The widget also reports every visitor as a live visitor (pages,
and the cursor while staff watch); this is on by default. Ask the human whether the site needs
consent for this kind of tracking. If it has a consent banner, load the widget only after
consent; if consent is needed but there is no banner, or they don't want it, turn it off with
`PATCH /api/v1/operator/tenant` `{ "liveVisitors": false }` (or Innstillinger in the admin).
Details in [widget.md](widget.md#live-visitors).

**Live view.** For staff's «Se live» to show the real page, the site must allow being framed by
the admin's origin: add `frame-ancestors 'self' $SNAKKBAR_HOST` (plus the origin of the site's own
admin, if you do step 4) to the site's CSP, and remove any `X-Frame-Options` header. See
[admin-embed.md](admin-embed.md#framing-the-customers-site).

Check, in a real browser on the site:
1. A round launcher appears bottom-right; no console errors.
2. Clicking it opens the chat with the assistant's name and welcome text.
3. Sending "Hei" streams an answer.
4. The network tab shows no `origin_not_allowed` (403) responses.
5. With live visitors on: `POST /api/v1/visitor/activity` goes out about every 10 s, and the
   visitor shows up in the admin under «Besøkende».

## 3. (Optional) Identified visitors

If the site has logged-in users, add a backend endpoint that returns a visitor token for the
current user, and call `Snakkbar.identify(fn)` after login and `Snakkbar.identify(null)` on
logout. Code in [widget.md](widget.md#identified-visitors). Send the user's `name`, `email`,
`phone`, `company` (and any `attributes` staff should see) with the token request; that is the
trusted source. Where the page knows the user but you can't mint tokens, use
`Snakkbar.setUser({ ... })` or `data-user-*` instead (unverified, see
[widget.md](widget.md#telling-staff-who-the-visitor-is)).

Check: log in on two browsers as the same user; the chat history (☰ menu) is the same in both,
and staff see the user's name on the visitor.

## 4. (Optional) Staff inbox inside the site's own admin

1. Backend: an authenticated endpoint that exchanges the logged-in staff user for an operator
   token (`POST $SNAKKBAR_HOST/api/v1/server/operator-tokens`). Map your user id to `externalId`
   and your admin flag to `role`. Code in [admin-embed.md](admin-embed.md#2-add-a-token-endpoint-to-your-backend).
2. Admin page: a container with a fixed height, `<script src="$SNAKKBAR_HOST/admin.js">` and
   `Snakkbar.mountAdmin(container, { host, token })` where `token` fetches from your endpoint and
   caches until shortly before `expiresAt`. Code in [admin-embed.md](admin-embed.md#3-mount-the-inbox).
3. The admin page's origin must be an allowed origin too (step 1).

Check:
1. The inbox loads and shows the staff member's name (Innstillinger → Profil).
2. Turn on *Tilgjengelig*; the widget launcher on the public site shows that person's face.
3. From the public site, write "kan jeg snakke med et menneske?" — the inbox alerts «Spør etter
   deg»; join and reply; the visitor sees the reply live.
4. Non-staff users get 401/403 from your token endpoint.

## 5. (Optional) Fully custom UI

Use the SDK instead of the stock UIs: [sdk.md](sdk.md) (visitor and staff clients, React hooks,
rendering rules) and [realtime.md](realtime.md). Follow the rendering rules in sdk.md so
translations, product cards, suggestions, navigation and receipts behave like the stock UI. On a
custom visitor UI, call `client.startTracking()` once per page load (on every page, so live
visitors and the live view work) and `client.updateProfile(...)` when the user is known; respect
the consent decision from step 2.

Check: the same scenario as step 4's checks works end to end with your UI.

## 6. Hand-over notes for the human

Tell the human:
- where the widget tag lives in the code base and how to remove it;
- the env vars you added (`SNAKKBAR_HOST`, `SNAKKBAR_SECRET_KEY`) and that the key must be rotated
  if it leaks;
- which origins you allowed;
- whether live visitors are on, and how consent is handled (or that `liveVisitors` is off), and
  whether you changed the site's CSP/`X-Frame-Options` for the live view;
- that assistant behaviour (name, portrait, tone, instructions, welcome text, knowledge) is
  managed in the Snakkbar admin, not in the site's code.

## Troubleshooting

| Symptom | Cause / fix |
| --- | --- |
| Launcher never appears | Script blocked (CSP, ad blocker) or wrong `data-tenant`. Check console and the `config` request. |
| `403 origin_not_allowed` | Add the exact origin (scheme + host + port) to allowed origins. |
| Answers never arrive, but no error | A proxy buffers `text/event-stream`. Exclude `/api/v1/*` from buffering, or call Snakkbar directly (it is a different origin anyway). |
| `401 unauthorized` in the embedded admin | Token endpoint returns an expired token; respect `expiresAt`. |
| Staff face never shows on the launcher | Staff must be *Tilgjengelig* and have the admin open (heartbeats keep them online). |
| Widget looks off-brand | Set the brand colour in the admin, or `data-accent="#rrggbb"`. |
| No visitors under «Besøkende» | `liveVisitors` is off, the widget is loaded only after consent that wasn't given, or (custom UI) `startTracking()` is never called. |
| «Se live» shows only a schematic page | The site refuses framing: allow the admin's origin in `frame-ancestors` and drop `X-Frame-Options`. |
