Architecture
Three things with three lifetimes — a CLI that writes an app, an app you own, and the Chatfuel API it talks to.
npx @chatfuel/wizard is a CLI that writes an app. When it finishes it is gone — no framework of
ours between you and your code, nothing in package.json pointing back at our repository, and
nothing we publish later that can change what is already on your disk.
So "Chatfuel SDK" is three things with three lifetimes, and most confusion about it comes from treating them as one.
@chatfuel/wizard ──writes──▶ your app ──talks to──▶ Chatfuel
(npx, once) (yours, forever) (GraphQL + WebSocket)The CLI runs once
The wizard's whole job is a directory copy with edits. It copies content/shell whole, deletes the
module subtrees you did not pick, regenerates the module registry, filters the navigation table,
prunes the tsconfig paths that no longer resolve, copies the source trees in, and rewrites the
marked import lines that point at them.
Module manifests are read at that moment and never again: there is no manifest, no registry and no plugin loader inside the app at runtime. What the manifests decided is already baked into the files you have.
The app is yours
What lands is a React + Vite project. Four trees inside it came from us as source rather than as
dependencies — the design system under ~ui, the typed API client under ~api, the schema
snapshot beside the client, and the proxy under vendor/chatfuel-proxy — and none of them is
reachable through node_modules. That trade is the subject of its own page: vendoring, not
depending.
The API it talks to
~api is a generated, typed GraphQL client: queries and mutations over HTTP, live updates over
graphql-ws, and REST only for uploads. The documents are generated from one SDL snapshot and
committed already built, so an app arrives with a typed client it never had to generate.
In the browser the client is created with no token at all — it points at your own origin and the proxy attaches the token server-side. See the token boundary.
Errors arrive in one envelope shape, and classification reads the GraphQL extensions.code rather
than the HTTP status. A module therefore never parses a transport error itself; it gets a typed
refusal with a code it can branch on.
Where the content comes from
The package on npm carries the module manifests and a commit with a sha256 for every file. The
trees themselves arrive over the network on the run that needs them, checked against those digests
before anything is written, and the commit a run landed on is recorded in the app's own
.chatfuel/lock.json. Where the content comes from is the whole
of it.
The token boundary
A Chatfuel token can read and change every bot in an account, so it never reaches the browser.
Every call from the app goes to its own origin under /chatfuel/*, and a proxy running on the
server side attaches the token there. That proxy is one source tree with three hosts — a Vite
plugin in development, a Node HTTP server on your own box, a serverless function on Vercel — and
all three share the request handling, the WebSocket relay and the log scrubber.
With the auth module the proxy also becomes a gate: a request must carry a Supabase session
whose user belongs to this tenant, or it is refused, and a half-configured deployment refuses
rather than letting anything through. Without it the boundary is half of one — the token stays on
the server, but the proxy has no callers to tell apart.
/admin is outside that gate either way. It has a door of its own — a password, a cookie and a
wrong-password counter — and what it opens is the whole account behind CHATFUEL_TOKEN, so a
Supabase user who may open nothing in the app can still try that password. Unset the password and
the routes are not mounted at all. The token boundary has the
mechanism.
What lives where in the repository
If you are reading the source rather than the app it writes, the top-level directories split on one question: does this end up on a user's disk?
| Path | What it is |
|---|---|
content/shell | the app template — the real app this repository runs, not a copy of one |
content/ui/src, content/api-client/src, content/vite-plugin-proxy/src | the source trees vendored into a scaffold |
content/schema | the SDL snapshot everything is generated from |
content/codegen | the generator body, copied into every app beside the client it produces |
content/modules/<id> | per module: the manifest, the handoff note, the skill and any migrations |
content/skills | the skills that belong to no module and are installed with every app |
packages/wizard | the published CLI |
packages/module-manifest | the JSON Schema every module.json is written against, and its type |
packages/design-system | the dev-only gallery for content/ui, never shipped |
scripts | the gates, and content-trees.ts — the exact list of what travels |
content-trees.ts is that list rather than a description of one, which is what settles the
question the layout raises: content/ui/src is a content tree and content/ui is not. The
package's configs, README and build scripts stay behind.
The other half of the split is worth stating outright, because it is the part that is not
guessable: packages/ is not "the libraries". It is what is built here and goes nowhere near a
user's project.
The React half of a module lives under content/shell/src/modules/<id>/, away from the rest of the
module — two halves in two places, which is the one thing about this layout that surprises people.
See modules.
What this costs you
Owning the code means owning the bugs in it. Nothing we publish can break your app, and equally
nothing we publish can fix it: an upgrade is never npm update. What the lock buys you is that it
can be a diff — chatfuel-wizard update reads the digest of every file it wrote and reports what
moved upstream, what you had already changed, and what it left alone. That is
upgrading an app.
How it fits together
Why the token never reaches the browser, why the code is copied in rather than installed, where the content comes from, and what a caller past the gate may still do.
The token boundary
A Chatfuel token can read and change every bot in an account, so it never leaves the server — and everything about the proxy follows from that.