Skip to content
ChatfuelSDK
Guides

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.

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.

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.

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.

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.

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:

{
  "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.

The bundler needs the same four:

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 enums 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.

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.

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:

@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 h1h6 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.

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.

Register the vendored plugin:

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.

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.

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.

ModuleEntry component
livechatLivechatApp
contactsContactsApp
dealsDealsApp
bookingsBookingsApp
knowledge-baseKnowledgeBaseApp
automationsAutomationsApp
coworkerCoworkerApp
flow-builderFlowBuilderApp
ads-optimizationAdsOptimizationApp
publishingPublishingApp
adminAdminApp
authAuthGate

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

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.

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.

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.

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.

Two more pages are worth having open while you wire: the token boundary for what the proxy is protecting and why VITE_ means public, and vendoring for what owning the source costs you at upgrade time.

On this page