Vendoring, not depending
The design system, the API client and the proxy arrive as source you own — the trade that makes, and the two rules that keep the copies working.
Nothing in your package.json points back at our repository. That is not an accident of packaging;
it is the decision the rest of this page explains.
Four trees are copied into your project as source:
| In this repository | In your app | Reached as |
|---|---|---|
content/ui/src | src/vendor/ui | ~ui |
content/api-client/src | src/vendor/api | ~api |
content/schema | src/vendor/schema | read from disk by codegen |
content/vite-plugin-proxy/src | vendor/chatfuel-proxy | relative paths |
A fifth is the generator itself: content/codegen lands at scripts/codegen, beside the client it
produces. It sits under scripts/ rather than src/ because it reads the disk — nothing in it is
part of the bundle, and the app's main tsconfig never sees it.
The proxy is copied recursively, with no file list, so a file added beside the others travels
without anyone remembering to name it. Four marked blocks are then rewritten to point at the
copies: the proxy import in vite.config.ts, server/entry.ts and api/chatfuel.ts, and the two
stylesheet imports in src/index.css.
What source you own is good for
A component you disagree with is a file you can edit. There is no upstream package to fight, no
!important to win an argument with, and no wrapper to write around a prop we did not expose.
The visual identity is CSS custom properties in vendor/ui/styles/tokens.css — colors, radii,
shadows, durations, the type ramp — so a rebrand is a token edit that every component follows,
rather than a search through class names. And deleting is as available as editing: a module you do
not want is a directory you remove, and the import boundary keeps that deletion local.
The other half of the trade is that nothing we publish can reach you. There is no version of the design system that can be pushed under your app and change how it renders on a Tuesday.
Two rules keep the copies portable
A tree that is going to be moved onto somebody else's disk has to survive the move byte for byte. Two rules make that true:
- Every internal import is relative. A path alias that resolves in this repository does not resolve after the copy lands somewhere else.
~uimay import nothing butreactandreact-dom. Those are the design system's only peer dependencies; anything else would be a package your app never agreed to install.
Both hold in the code, and neither is checked by anything. The repository's own gates read
content/ui/src for tokens, barrels and class names, never for import specifiers, and the one
import-boundary pass is scoped to module subtrees. They are conventions kept by hand — break one
and you find out when the tree lands in somebody's app and does not resolve.
The API client's contract is the same shape with a different list: its source imports graphql,
graphql-ws and @graphql-typed-document-node/core, so a scaffolded app declares exactly those
three as dependencies. The proxy adds ws, undici and https-proxy-agent, and every relative
import inside it carries an explicit .js extension because Vercel deploys those files as they
are.
So "vendored" does not mean dependency-free. It means the dependency list is short, visible in your
own package.json, and yours.
What does not travel
Our unit tests for the copied trees stay behind — the copy filter drops every *.test.ts and
*.test.tsx on the way out, because a test of ours is a failing vitest run in an app nobody
wrote it for.
The shell is the exception and travels whole, tests included. That suite is the app's own: it is what catches a module that renders nothing, and it is yours to extend.
Neither does the manifest tooling travel: module manifests are read while the wizard runs and have
no presence in the app afterwards. The same line runs through the whole repository — a package's
src/ is what a user gets, and the configs, README and build scripts beside it stay here.
The cost, stated plainly
An upgrade is never npm update. A fix we make after your run does not reach you until you go and
get it.
What makes going and getting it bearable is that every file the wizard wrote is recorded, with the
digest it had when it landed, so chatfuel-wizard update can tell a file you changed from a file
that only moved upstream — see upgrading an app. That is the price of the
guarantee above, and it is the same price either way: you cannot have code that we cannot break and
also code that we can fix.
For the tree this leaves on disk, see what the wizard writes.
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.
Where the content comes from
The published wizard carries a commit and a digest per file rather than the files themselves, follows a branch from there, and refuses a branch that has moved backwards.