Skip to content
ChatfuelSDK
Concepts

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?

PathWhat it is
content/shellthe app template — the real app this repository runs, not a copy of one
content/ui/src, content/api-client/src, content/vite-plugin-proxy/srcthe source trees vendored into a scaffold
content/schemathe SDL snapshot everything is generated from
content/codegenthe 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/skillsthe skills that belong to no module and are installed with every app
packages/wizardthe published CLI
packages/module-manifestthe JSON Schema every module.json is written against, and its type
packages/design-systemthe dev-only gallery for content/ui, never shipped
scriptsthe 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.

On this page