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 --embedThe 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 ModuleClientEverything 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 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.
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.
| 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:
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:
- The dev server starts with no missing-module errors — the aliases resolve.
- The mounted module renders styled, not as unstyled HTML — the tokens reached the CSS graph.
- In the network tab, requests go to
/chatfuel/graphqland come back200, and the Chatfuel token appears in no request the browser makes — the proxy is mounted and the boundary holds. - 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.
Change how it looks
One file of tokens moves every component in both themes. The duplicate-block rule, the two places motion lives, the fonts that are the app's rather than the system's, and the edits that generate nothing.
Make it yours
The app's name and mark live in two environment variables and one file in public/ — except the browser tab, which is edited in index.html on disk because the head is parsed before any environment is read.