By the end of this page, the assistant will be on every page of your app, answering as your agent. There are seven steps. Do them in order, because each step builds on the one before.

Each step follows from [how the assistant works](/assistant-ui). If you haven't read that page, read it first. It's short.

## Before you start

The assistant can't answer until three things are true, and none of them is in your code:

1. The agent is published. A key for an agent with no published revision gets no answers.
2. You have a publishable key for that agent. Create one in the **Install** section of the agent's page in the dashboard.
3. The key allows the origin you're testing on. An origin is a scheme, host and port, so `http://localhost:5173` and `http://localhost:3000` are different origins. Origins are fixed when you create the key, so list your local and staging origins then. See [Publishable keys](/publishable-keys).

## 1. Choose how to load it

| If your app is…                                                                    | Use             |
| ---------------------------------------------------------------------------------- | --------------- |
| React with a bundler: Next.js, Vite, Remix and others                              | the npm package |
| Anything else: Vue, Svelte, Angular, Rails, Django, Laravel, WordPress, plain HTML | the script tag  |

The script tag isn't a lesser version. It's the same assistant with React bundled inside. In a React app, the npm package uses your copy of React, stays on the version you installed, and accepts React components where the script tag needs plain objects. The script tag isn't pinned: `/v1/` always serves the current release.

Use one or the other on a page, never both. Two copies of the assistant won't share a conversation.

## 2. Find the one place that is on every page

> Mount the assistant exactly once, in the part of your app that wraps every page and stays mounted while users navigate. Don't put it inside a page, don't put it in a component that only some routes render, and don't mount it twice.

The live conversation belongs to the mounted assistant, so each of those mistakes breaks something users will see:

| If you mount it…                       | What happens                                                                                                                                                                                                         |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| inside a page component                | It unmounts on every navigation. The connection closes, a new token is minted, the transcript replays and an open palette closes. A user who asks a question and then clicks a link watches the conversation reload. |
| in a component only some routes render | It disappears on every other route, often including the page the agent has just sent the user to.                                                                                                                    |
| twice                                  | Two launchers appear in the corner.                                                                                                                                                                                  |

To find the right place, look for the component or template that sits *above* whatever swaps one page for the next. Depending on your stack, that's the router outlet, `{children}`, `<Outlet/>`, `<router-view>`, `{% block content %}` or `yield`. The assistant goes beside that, not inside it.

| Your app                                           | The right place                        | A common wrong place                      |
| -------------------------------------------------- | -------------------------------------- | ----------------------------------------- |
| Next.js App Router                                 | `app/layout.tsx`                       | `app/page.tsx`, or a route group's layout |
| Next.js Pages Router                               | `pages/_app.tsx`                       | any page in `pages/`                      |
| React Router                                       | the root component, outside `<Routes>` | a route's `element`                       |
| Vue                                                | `App.vue`, beside `<router-view>`      | a view component                          |
| Server-rendered: Rails, Django, Laravel, WordPress | the base layout every page extends     | a single page's template                  |

In a server-rendered app, every link loads a whole new page, so the assistant mounts fresh on each page wherever you put it. That's expected: it resumes the conversation from the browser and carries on. The base layout is still the right place, because it's the one place that appears on every page.

## 3. Add it

### npm package

```sh
npm install @teaflask/assistant
```

Import the stylesheet once, and render the assistant in the place you found in step 2:

```tsx
import type { ReactNode } from "react";
import { TeaflaskAssistant } from "@teaflask/assistant";
import "@teaflask/assistant/styles.css";

export function AppShell({ children }: { children: ReactNode }) {
  return (
    <>
      {children}
      <TeaflaskAssistant publishableKey="pk_live_…"/>
    </>
  );
}
```

The key is meant to be public, so write it inline or read it from a public environment variable. The package needs React 18.3 or 19, listed under [Requirements](#requirements); everything else it depends on comes with it.

The components render on the server and connect once they mount in the browser, so you don't need to turn off server rendering. In the Next.js App Router, the root layout can stay a Server Component. The one limit is React's: a Server Component can't pass a function to a Client Component. Once you pass `onNavigate`, `getEndUserToken` or an adapter, put the assistant in a small Client Component:

```tsx
// app/assistant.tsx
"use client";

import { useRouter } from "next/navigation";
import { TeaflaskAssistant } from "@teaflask/assistant";

export function Assistant() {
  const router = useRouter();
  return (
    <TeaflaskAssistant
      publishableKey="pk_live_…"
      onNavigate={(path) => router.push(path)}
    />
  );
}
```

```tsx
// app/layout.tsx
import type { ReactNode } from "react";
import "@teaflask/assistant/styles.css";
import { Assistant } from "./assistant";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Assistant/>
      </body>
    </html>
  );
}
```

### Script tag

The script tag has two parts, and they can go in different files:

| Part                               | Goes in                                                                                                                                          |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| The loader `<script>`              | The HTML every page is served from, just before `</body>`. In a single-page app that's `index.html`; in a server-rendered site, the base layout. |
| The `<teaflask-assistant>` element | The place you found in step 2. In a server-rendered site, that's the same base layout.                                                           |

```html
<script
  type="module"
  src="https://app.teaflask.com/assistant/v1/assistant.js"></script>

<teaflask-assistant publishable-key="pk_live_…"></teaflask-assistant>
```

`type="module"` is required. You don't need a stylesheet or React.

In a framework that renders templates, such as Vue, Svelte or Angular, the element behaves like any other custom element in your shell template. Some frameworks have to be told that the tag is a custom element and not one of their own components: in Vue that's `compilerOptions.isCustomElement` (with Vite, an option of the Vue plugin), and in Angular it's `CUSTOM_ELEMENTS_SCHEMA`.

## 4. Give it its settings

Both ways of loading the assistant take the same settings. The only difference is how you pass them.

### npm package

Every setting is a prop on `<TeaflaskAssistant>`, as in the examples above.

### Script tag

Settings that are plain text are attributes: `publishable-key`, `base-url`, `mode`, `corner`, `hotkey` and `companion-mark-src`. Everything else, meaning functions, objects and renderers, is a JavaScript property that you set on the element:

```html
<script type="module">
  const assistant = document.querySelector("teaflask-assistant");

  assistant.onNavigate = (path) => myRouter.push(path);
  assistant.onError = (error) => reportError(error);
</script>
```

Here `myRouter` and `reportError` stand for your own code. You set `userId`, `getEndUserToken`, `actionsAdapter` and `toolViews` the same way, and each one's page explains what to set it to.

The element picks up any property set before the loader finished, so the order of your scripts doesn't matter. Set a property again whenever its value changes. For example, set `userId` and `getEndUserToken` when a user signs in, and set both to `null` when they sign out.

If your Content Security Policy blocks inline scripts, put this setup in a module file that your policy already allows. That file can't see your server's variables. To pass it a server-side value, such as the signed-in user's id, render the value into the page as data, for example a `data-` attribute on the element or a JSON script block, and read it from the script.

In a template framework, set properties through the framework's reference to the element once it has mounted: a template ref, `bind:this` or `@ViewChild`.

The element accepts every setting the React component does, with these exceptions:

- It can't take React components. A tool view on the element is a plain object with `mount`, `update` and `destroy`; see [Tool views](/tool-views).
- It takes your mark as an image URL in `companion-mark-src`, not as a component.
- It has no `onTelemetry`.
- The full-page element has no `initialPrompt`, `welcomeTitle`, `welcomeMark` or `renderChrome`.
- The package's functions and hooks, such as `resetAssistant`, the activity hook and the headless hooks, are npm only.

### All settings

| Setting                                      | npm prop                                        | Element attribute                   | Element property            | Default                        | More                         |
| -------------------------------------------- | ----------------------------------------------- | ----------------------------------- | --------------------------- | ------------------------------ | ---------------------------- |
| Publishable key                              | `publishableKey`                                | `publishable-key`                   |                             | required                       | [Keys](/publishable-keys)    |
| API address                                  | `baseUrl`                                       | `base-url`                          |                             | `https://api.teaflask.com`     |                              |
| Color mode: `light`, `dark`, `auto`          | `mode`                                          | `mode`                              |                             | `light`                        | [Theming](/appearance)       |
| Widget corner: `bottom-right`, `bottom-left` | `corner`                                        | `corner`                            |                             | `bottom-right`                 | [Surfaces](/surfaces)        |
| Palette shortcut                             | `hotkey`                                        | `hotkey` (`"false"` turns it off)   |                             | `mod+k`                        | [Surfaces](/surfaces)        |
| Your mark                                    | `companionMark` (a node)                        | `companion-mark-src` (an image URL) |                             | the Teaflask flask             | [Theming](/appearance)       |
| Navigation                                   | `onNavigate`                                    |                                     | `onNavigate`                | see step 5                     | step 5                       |
| Signed-in user                               | `userId`, `getEndUserToken`                     |                                     | `userId`, `getEndUserToken` | anonymous                      | [Identity](/identity)        |
| API calls                                    | `actionsAdapter`                                |                                     | `actionsAdapter`            | none                           | [Actions adapter](/adapters) |
| Result renderers                             | `toolViews`                                     |                                     | `toolViews` (plain objects) | built-in display               | [Tool views](/tool-views)    |
| Theme tokens                                 | `theme`                                         |                                     | `theme`                     | package defaults               | [Theming](/appearance)       |
| Opening prompts                              | `suggestions`                                   |                                     | `suggestions`               | the agent's configured prompts | [Surfaces](/surfaces)        |
| Errors                                       | `onError`                                       |                                     | `onError`                   | none                           |                              |
| Analytics events                             | `onTelemetry`                                   |                                     |                             | none                           |                              |
| Open the palette                             | `openPalette()` on a ref to `TeaflaskAssistant` |                                     | `openPalette()`             |                                | [Surfaces](/surfaces)        |

## 5. Tell it how to navigate

When the agent takes a user to a page, it gives your app an app-relative path such as `/settings/billing`, and your app does the navigating. If you haven't said how, what happens depends on which way you loaded the assistant:

| `onNavigate` is… | npm package                                | Script tag                                 |
| ---------------- | ------------------------------------------ | ------------------------------------------ |
| not set          | the agent is told this page can't navigate | a full page load to the path               |
| a function       | your function is called with the path      | your function is called with the path      |
| `null`           | —                                          | the agent is told this page can't navigate |

The defaults differ on purpose. A page using the script tag can always fall back to an ordinary page load. A React app usually has a client-side router the package can't see, so the package doesn't guess.

In a single-page app, pass your router's navigate function so the page doesn't reload, as the Next.js example in step 3 does. If your React app has no client-side router, pass `(path) => window.location.assign(path)`. The function can return a promise that resolves once your router has finished.

The agent also needs its navigation tool turned on, with the paths it may visit, under [App access](/app-access). `onNavigate` is how your app navigates; the agent setting is whether the agent may.

## 6. Let it through your Content Security Policy

Skip this step if your site doesn't send a `Content-Security-Policy` header. If it does, add these sources to your existing policy:

| Directive     | Add                                                                     | Why                                                                                         |
| ------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `script-src`  | `https://app.teaflask.com`                                              | Script tag only: the loader and the files it imports                                        |
| `connect-src` | `https://api.teaflask.com`, or the assistant's `baseUrl` if you set one | The token exchange and the streamed conversation                                            |
| `connect-src` | Your API's origin, if you add an [actions adapter](/adapters)           | The adapter's requests, which the browser sends to your API                                 |
| `connect-src` | The upload address, if you turn on attachments                          | Files upload from the browser straight to storage, at an address the upload request returns |
| `img-src`     | `blob:`                                                                 | Previews of images the user attaches                                                        |

The `theme` setting writes inline styles; [Theming](/appearance) explains what that needs under a strict policy. When your policy blocks something, the browser console shows a violation that names the directive.

## 7. Check that it works

Open a page on one of the key's allowed origins and work through these checks in order.

### The launcher appears in the corner

If it doesn't appear, one of these is the cause:

| Cause                                    | How you can tell                                    |
| ---------------------------------------- | --------------------------------------------------- |
| The assistant isn't mounted on this page | No element or component in the page; revisit step 2 |
| The loader was blocked                   | A CSP violation in the console; revisit step 6      |
| The element has no `publishable-key`     | A warning in the console                            |

### The agent answers a message

If the assistant shows an error, its text names the problem. Look at the token request in the browser's Network panel:

| Response                 | Meaning                                    |
| ------------------------ | ------------------------------------------ |
| `403 ORIGIN_NOT_ALLOWED` | This page's origin isn't on the key's list |
| `401`                    | The key is wrong or has been revoked       |

If the token request succeeds but no reply arrives, check that the agent is published, that its model is configured, and that it doesn't require sign-in while you're testing anonymously.

### The dashboard records the session

In the agent's **Install** section, the key's row changes from **Waiting for the first session…** to **Session seen …**. This only confirms the token exchange, and the row can change even when replies fail, so the previous check is the one that counts.

If you set up navigation, ask the agent to take you somewhere.

## Requirements

### npm package

The package needs these peer dependencies. npm installs missing peers automatically; some other package managers don't.

| Package     | Version                |
| ----------- | ---------------------- |
| `react`     | `^18.3.0` or `^19.0.0` |
| `react-dom` | `^18.3.0` or `^19.0.0` |

Everything else the package uses, including `use-stick-to-bottom`, is a plain dependency that your package manager installs with it.

Your build needs Node.js 20 or newer.

### Browsers

The assistant needs a current browser with streaming `fetch` and the native `<dialog>` and popover APIs. The script tag also needs custom elements, Shadow DOM and constructable stylesheets. There's no tested list of minimum versions, so try it in the browsers and embedded WebViews your app supports.

## What to wire next

The assistant now answers questions from anonymous visitors. Each of these is optional, and each one unlocks something:

| Wire                         | To get                                                                     |
| ---------------------------- | -------------------------------------------------------------------------- |
| [Identity](/identity)        | Conversations that belong to a signed-in user, with history across devices |
| [Actions adapter](/adapters) | The agent calling your API as the user                                     |
| [Tool views](/tool-views)    | Results shown as your own components instead of the default display        |
| [Surfaces](/surfaces)        | A full-page assistant on a route of your own, or a different shortcut      |
| [Theming](/appearance)       | Your colors, fonts and mark                                                |
