# Put the modules in an app you already have

> npx @chatfuel/wizard --embed copies a namespaced footprint into your project and wires nothing. The five things you wire, for a Vite host and for a Next.js one, and what a Next.js route handler cannot do.

Page: https://sdk.chatfuel.com/docs/guides/embed-into-an-existing-app
Markdown: https://sdk.chatfuel.com/docs/guides/embed-into-an-existing-app.md

A Chatfuel token can read and change every bot in an account, so it never reaches the browser.
That one fact shapes everything below: the modules talk to your own origin under `/chatfuel/*`,
and a proxy running on your server side attaches the token there. Embedding them into an app you
already have is mostly the work of giving that proxy somewhere to live.

```bash
npx @chatfuel/wizard --embed
```

The wizard copies a self-contained, namespaced footprint into your project and wires none of it.
It writes no `vite.config`, no `tsconfig` and no CSS; the only files of yours it touches are
`.env`, `.gitignore` and your coding agent's instructions file, and every one of those it appends
to rather than rewrites. What you end up with is a Chatfuel surface mounted inside your app, on
your routes, with your own chrome around it.

<Steps>
  <Step>
    ### Run it, and read the footprint [#run-it-and-read-the-footprint]

    `--embed` is the only thing that selects embed mode; `--yes` on its own stays standalone. The
    target needs a `package.json`, and the wizard refuses to run if `src/chatfuel/` is already there
    rather than merging into it.

    ```
    src/chatfuel/
    ├── modules/types.ts              # the module contract (ModuleAppProps)
    ├── modules/<id>/…                # each selected module's source
    ├── vendor/ui/…                   # design system (Tailwind v4 tokens + components)
    ├── vendor/api/…                  # typed GraphQL client + generated documents
    ├── vendor/chatfuel-proxy/…       # the proxy, one file per concern; core.ts assembles them
    └── client.ts                     # createAppClient() — proxy-mode ModuleClient
    ```

    Everything in there is vendored source: your project owns every line and you edit it freely.
    Module code imports React, `~ui`, `~api` and its own files, and nothing else.

    Outside that directory the wizard writes four small things. `.env` gains only the keys it does not
    already hold, under a `# Added by chatfuel-wizard` comment, and a key that exists with a different
    value is reported rather than overwritten. `.gitignore` gains a `.env` line if it has none — the
    wizard asks first, and refuses to write your token to disk if you decline. The skills go to
    `.claude/skills/` or `.agents/skills/`. And `CLAUDE.md` or `AGENTS.md` gains a section between
    `<!-- chatfuel:begin -->` and `<!-- chatfuel:end -->` — appended if the file exists, refreshed in
    place on a later run, with your own instructions outside the markers untouched. With the `auth`
    module there is a fifth: `supabase/chatfuel/`, in its own subdirectory because you may have a
    `supabase/` of your own.
  </Step>

  <Step>
    ### Install what it printed [#install-what-it-printed]

    The run ends with the exact command for whichever package manager your lockfile names. It offers
    to run it for you, and it will not do that under `--yes` — an unattended run must not write to a
    project it did not create.

    The runtime set is `graphql`, `graphql-ws`, `@graphql-typed-document-node/core` and the three
    font packages `@fontsource-variable/geist`, `@fontsource-variable/geist-mono` and
    `@fontsource-variable/manrope`. The development set, for a Vite host, is `ws`, `@types/ws`,
    `tailwindcss` and `@tailwindcss/vite`. On top of that comes whatever the modules you picked
    declare — today only `auth`, which needs `@supabase/supabase-js`.

    Your project has to already have `react` and `react-dom`; the wizard warns if your `package.json`
    does not declare them.
  </Step>

  <Step>
    ### Alias `~ui` and `~api` [#alias-ui-and-api]

    The modules reach the design system and the client through two aliases, never through
    `node_modules`. Point them at the copied trees in your TypeScript config — the one that includes
    `src`, which in a fresh Vite template is the app-side of the split configs:

    ```jsonc
    {
      "compilerOptions": {
        "paths": {
          "~ui": ["./src/chatfuel/vendor/ui/index.ts"],
          "~ui/*": ["./src/chatfuel/vendor/ui/*"],
          "~api": ["./src/chatfuel/vendor/api/index.ts"],
          "~api/*": ["./src/chatfuel/vendor/api/*"]
        }
      }
    }
    ```

    `paths` resolve relative to the config file, so do not add `baseUrl` — recent TypeScript
    deprecates it and fresh Vite templates error on it.

    <Tabs items="['Vite', 'Next.js']">
      <Tab value="Vite">
        The bundler needs the same four:

        ```ts
        import path from 'node:path';

        resolve: {
          alias: [
            { find: /^~ui$/, replacement: path.resolve('src/chatfuel/vendor/ui/index.ts') },
            { find: /^~ui\//, replacement: `${path.resolve('src/chatfuel/vendor/ui')}/` },
            { find: /^~api$/, replacement: path.resolve('src/chatfuel/vendor/api/index.ts') },
            { find: /^~api\//, replacement: `${path.resolve('src/chatfuel/vendor/api')}/` },
          ],
        },
        ```

        Three frictions show up in a fresh `npm create vite` react-ts template and are worth fixing before
        you start reading error messages: `erasableSyntaxOnly` is on and rejects the generated `enum`s in
        `vendor/api/generated/`, so set it to `false` in both split configs; the node-side config
        type-checks `vite.config.ts` and the vendored proxy uses fetch types, so add `"DOM"` to that
        config's `lib`; and `module: "nodenext"` there means the proxy import needs its explicit
        extension.
      </Tab>

      <Tab value="Next.js">
        The TypeScript `paths` above are enough — webpack and turbopack both honour them, and there is no
        second place to write them.
      </Tab>
    </Tabs>

    If your project already uses `~ui` or `~api` for something of its own, rename them. They are plain
    strings in the copied sources, so a grep over `src/chatfuel` and a rename in both the sources and
    the config settles it.
  </Step>

  <Step>
    ### Wire the Tailwind v4 CSS entry [#wire-the-tailwind-v4-css-entry]

    The components use Tailwind v4 utilities plus the tokens in
    `src/chatfuel/vendor/ui/styles/tokens.css`. That file contributes theme variables only, and it has
    to be imported into the same CSS graph as your `@import "tailwindcss"` — separate CSS entries
    compile independently in v4. In the host's global CSS:

    ```css
    @import "tailwindcss";
    @import "@fontsource-variable/geist";
    @import "@fontsource-variable/geist-mono";
    @import "@fontsource-variable/manrope";
    @import "./chatfuel/vendor/ui/styles/tokens.css";
    @source "./chatfuel";
    ```

    `@source` is what makes Tailwind scan the copied sources for class names; adjust the relative
    paths to wherever your CSS entry lives.

    There is a fourth file next to those, `vendor/ui/styles/base.css`, and it is deliberately not in
    the block. It paints `<body>`, restyles every `h1`–`h6` on the page to the display face, and
    claims `::selection` and every scrollbar — right for a standalone app, and a takeover of somebody
    else's. Import it only if you want the Chatfuel look for the whole document.

    If your host is already on Tailwind v4, the token import and `@source` are the whole job. On v3
    there is no clean path: v3 cannot read v4 `@theme` tokens, so you either upgrade the host or build
    a small standalone stylesheet for the Chatfuel tree with the v4 CLI and scope it. With no Tailwind
    at all, add `@tailwindcss/vite` — or the PostCSS plugin outside Vite — and create the entry above.

    Three theming decisions are yours rather than ours. The dark palette applies on `[data-theme]`
    anywhere, deliberately not `:root`-prefixed, so you can stamp the attribute on the wrapper element
    holding the Chatfuel tree instead of on `<html>`; `useTheme({ target })` takes that element, and
    `persist: false` stops it fighting a preference you already store. `color-scheme` travels with the
    attribute, which is usually what you want. And do not mount `ThemeToggle` unless you meant to —
    two theme switchers on one page is a bug, and reading your own preference into
    `useTheme().setPreference` is normally the right integration.
  </Step>

  <Step>
    ### Mount the proxy [#mount-the-proxy]

    Every module call goes to same-origin `/chatfuel/graphql` (GraphQL over HTTP and over WebSocket)
    and `/chatfuel/api/*` (REST uploads). `CHATFUEL_TOKEN` lives in `.env` and is injected server-side
    there. The browser client carries no Chatfuel token at all — when the auth gate is on, what it
    sends is the caller's own session token so the proxy can check it.

    <Tabs items="['Vite', 'Next.js']">
      <Tab value="Vite">
        Register the vendored plugin:

        ```ts
        import { chatfuelProxy } from './src/chatfuel/vendor/chatfuel-proxy/vite';
        // in plugins: [...]
        chatfuelProxy(),
        ```

        It reads `CHATFUEL_TOKEN` and `CHATFUEL_API_BASE` off the environment — through Vite's `loadEnv`
        with an empty prefix, so unprefixed secrets never reach the client bundle — and relays WebSockets
        with its own `connection_init`. `vite.ts` is the only file in that directory that imports `vite`;
        everything else is plain Node, and `core.ts` assembles the proxy from the per-concern files beside
        it. The same core is what the bundled production server and the Vercel function mount.
      </Tab>

      <Tab value="Next.js">
        Add an HTTP route handler that forwards `/chatfuel/graphql` and `/chatfuel/api/*` to
        `${CHATFUEL_API_BASE}` with `Authorization: Bearer ${process.env.CHATFUEL_TOKEN}`.

        **A route handler cannot relay a WebSocket.** The modules that subscribe — `livechat`, `coworker`,
        `deals`, `bookings` — need a sidecar relay running beside the app, and the core skill carries the
        spec for one. Say so before you promise live updates on a Next.js host; there is no configuration
        that makes a route handler do it.

        Whatever host stack you wire, the browser's `Authorization` header must never be forwarded
        upstream.
      </Tab>
    </Tabs>

    Two behaviours of the proxy are worth knowing before you point it at production. **The bot fence**:
    by default it asks Chatfuel which bots the token's account owns, caches the answer for a minute,
    and refuses a request naming anything else. Pass `allowedBotIds: [...]` to freeze that list, or
    `'any'` to turn the check off for a host that fences on its own. **The auth gate**: set both
    `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` and every request has to carry a Supabase session
    whose user belongs to the tenant before the Chatfuel token is injected. Neither of them is open
    mode. One without the other fails closed — every request answers `ProxyAuthMisconfigured` — which
    is the behaviour you want from a half-configured gate.
  </Step>

  <Step>
    ### Mount an entry component [#mount-an-entry-component]

    Every module exports one component that takes `ModuleAppProps`: `botId`, `client`, `params`,
    `setParams`, `view`, `setView` and `navigate`. `view` is the module's own surface, `''` being its
    default one, and `navigate` is how it asks to go somewhere else — in an embed, that is your
    router's job. `selectBot` is absent, because there is no app around the module to re-point.

    | Module             | Entry component      |
    | ------------------ | -------------------- |
    | `livechat`         | `LivechatApp`        |
    | `contacts`         | `ContactsApp`        |
    | `deals`            | `DealsApp`           |
    | `bookings`         | `BookingsApp`        |
    | `knowledge-base`   | `KnowledgeBaseApp`   |
    | `automations`      | `AutomationsApp`     |
    | `coworker`         | `CoworkerApp`        |
    | `flow-builder`     | `FlowBuilderApp`     |
    | `ads-optimization` | `AdsOptimizationApp` |
    | `publishing`       | `PublishingApp`      |
    | `admin`            | `AdminApp`           |
    | `auth`             | `AuthGate`           |

    The smallest mount there is, with no deep-link routing:

    ```tsx
    import { useMemo, useState } from 'react';
    import { createAppClient } from './chatfuel/client';
    import { LivechatApp } from './chatfuel/modules/livechat/LivechatApp';

    export function ChatfuelPanel() {
      const client = useMemo(() => createAppClient(), []);
      const [params, setParams] = useState(new URLSearchParams());
      const [view, setView] = useState('');
      return (
        <LivechatApp
          botId={botId /* whichever bot the host app is showing */}
          client={client}
          params={params}
          setParams={setParams}
          view={view}
          setView={(next, nextParams) => {
            setView(next);
            if (nextParams) setParams(nextParams);
          }}
          /* One module and no router of your own: a link into another module has
             nowhere to go. Point this at your router in the variant below. */
          navigate={() => undefined}
        />
      );
    }
    ```

    Back `params`/`setParams` with your router's search params, `view`/`setView` with a segment of
    your own route and `navigate` with your router's push, and a module's deep links become shareable
    URLs — livechat's `?c=<conversationID>` opens a conversation directly. Each module's own playbook,
    inside its installed skill, names its params and says whether it needs the WebSocket relay.

    On Next.js the modules are client components: mount them behind `'use client'`, and replace the
    `import.meta.env.*` reads with your public env convention where you wire `botId`.
  </Step>

  <Step>
    ### Verify [#verify]

    Four checks, in this order, because each one rules out the layer below it:

    1. The dev server starts with no missing-module errors — the aliases resolve.
    2. The mounted module renders **styled**, not as unstyled HTML — the tokens reached the CSS graph.
    3. In the network tab, requests go to `/chatfuel/graphql` and come back `200`, and the Chatfuel
       token appears in no request the browser makes — the proxy is mounted and the boundary holds.
    4. For a module that subscribes, live updates arrive — the WebSocket relay works.

    Check three is the one to do deliberately rather than by eye. Filter the network tab for the
    token's first characters and expect nothing; that is the whole point of the arrangement, and it is
    the check that catches a proxy you accidentally wired as a plain rewrite.
  </Step>
</Steps>

<Callout type="warn">
  Leave the three `@fontsource-variable` imports out and nothing fails. `--font-sans` and
  `--font-display` name faces the browser cannot find, the embedded UI falls back to your host's
  system stack, and it looks close enough to right that people ship it. They are npm packages,
  self-hosted, no third-party origin at runtime — install them and import them beside the token
  import.
</Callout>

## What this is not [#what-this-is-not]

Embed mode is for an app that is not ours. If the wizard wrote the project and you want one more
module in it, the same command is the delivery mechanism but the ending is different — see
[adding a module to an app you already made](/docs/guides/add-a-module).

Two more pages are worth having open while you wire: [the token boundary](/docs/concepts/token-boundary)
for what the proxy is protecting and why `VITE_` means public, and
[vendoring](/docs/concepts/vendoring) for what owning the source costs you at upgrade time.
