Core
The module with no interface — the skill every other module is built on, and the transport, error and pagination rules the whole app inherits.
What it is
core has no interface. There is no content/shell/src/modules/core/, no route, no rail item,
and nothing in the manifest that would mount one. What it installs is a skill — the one every
other chatfuel-* skill declares as required — and what that skill holds is the set of facts
nobody should have to rediscover per module: how you reach the API, what a token is, what an
error looks like, how a connection pages, the whole schema as a file, and a script that checks
an operation against it before you run it.
That is also why it is the one module you never choose. It is installed with everything.
Installed as
| Wizard id | core |
| Install it | You don't — --modules never needs it |
| Selected by | Always. Every run installs it. |
| Requires | Nothing |
| Recommends | — |
| Skill | chatfuel-core |
Neither hidden nor railHidden, because neither flag applies: those describe how a module
appears in the shell, and this one has no shell presence to describe. core is implicit for
every module and is never listed in a requires array — the wizard's dependency closure calls
visit('core') before anything the caller asked for, so a manifest that named it would be
saying something the registry already guarantees.
Routes and views
There is no route. The manifest carries no app block at all, which is what a module uses
to declare an entry component, an embed root and its environment. Without one there is nothing
to mount, nothing to navigate to, and nothing to delete when a scaffold drops a module.
The model
Three inputs, and everything else follows from them: a base URL (https://panel.chatfuel.com
in production), an API token, and a botID. There is one endpoint per environment, not
per account:
| HTTP | POST {base}/graphql |
| WebSocket | wss://{host}/graphql, subprotocol graphql-transport-ws |
| REST | {base}/api/… — file uploads |
Authentication is Authorization: Bearer <token> over HTTP and REST. A browser cannot set
headers on a WebSocket, so there the token rides in the connection_init payload as
authToken, with the Bearer prefix included — on your server's own socket to the API, never
in a browser. The credential is a dashboard user token that the account owner generates in
the Chatfuel panel — treat it as opaque, with no assumed shape or lifetime. A bot's own
apiToken is a different credential entirely: it is scoped to one bot, it works only on the
separate REST APIs, and it reaches Chatfuel with no proxy in front of it, which is why the
proxy's gate refuses a selection that would read it out of the schema.
A proxy is not optional for anything with a browser in it. Production CORS allows exactly one
origin, https://panel.chatfuel.com, so a browser app on any other domain fails the preflight
for POST /graphql — and the panel's frame-ancestors policy rules out an iframe too.
Server-to-server integrations involve no CORS and need no proxy. The asymmetry worth knowing: a
raw WebSocket is not subject to preflight, so subscriptions could connect from a foreign
origin — which would require shipping a full-account credential to the browser, so proxy both.
There is one token, it holds the whole account, and it does not tell the deployment's customers apart. Nothing upstream will draw a tenant boundary for you, which is why the proxy is the thing that decides which bot a caller may name rather than asking the caller. That is the constraint the whole app is shaped around; the deployed form of it is on the token boundary, and what a caller past the gate may still do is on ceilings and fences.
Four things about the graph are worth knowing before you write a query. The hierarchy is
UserAccount → Workspace → Bot → ContactScope → Contact → Conversation, and almost everything
is scoped by botID: there are twelve root Query fields, so you read data by traversing
bot(id:) and currentUser rather than by per-entity root queries. Message types, send
mutations and several fields are per-platform, so branch on Conversation.platform or
__typename. Conversation.id is the contact id — a server-side alias, and every
conversationID argument takes the contact's id. And real time is GraphQL subscriptions over
one lazy WebSocket, with lists arriving as Add/Update/Remove edge events the client merges and
re-sorts itself.
A 401-equivalent can arrive inside an HTTP 200. The API may answer 200 with an errors[]
array carrying an Unauthorized code, so a client that only checks the HTTP status treats a
dead token as an empty result. Scan errors[].extensions.code — and one level deeper: a
relayed failure carries a generic message at the top and the real code and traceId in
extensions.errors[]. data and errors can also coexist, so partial data is a case each
view has to decide about rather than a state that cannot happen.
Pagination is Relay-shaped and then not: every connection has its own cursor scalar and its
own PageInfo type — there is no shared one — cursors are opaque, and first is required
on some fields and optional on others. Conversation.messages is the one to read the signature
for: with no cursor or with after it runs newest-first and after walks backwards into
history, while before runs ascending.
Configuration
The manifest declares no app.env at all, so this module contributes no environment
variable of its own. What the app actually reads comes from the modules you installed beside it
— in practice CHATFUEL_TOKEN, VITE_CHATFUEL_WORKSPACE_ID and CHATFUEL_API_BASE, which
every surface module declares. All of them, and which side reads each, are on
environment variables.
Permissions
The manifest declares no permissions. core asks for no Chatfuel object and no action,
because it performs no operation — the modules built on it do.
The permission model it documents is per-bot and per-role (Admin | Editor | Agent | Custom),
with (object, action) pairs: Inbox gates live chat, People gates contacts, and
ContactsAssignedToOthers / ContactsUnassigned restrict visibility — a restricted agent
receives UnavailableContact stubs rather than an error. Check what a token can actually do
with the MyBotRole operation before building a screen that assumes edit rights.
Limits
The API rate-limits per token, and the ceiling is not published in the schema. Batch reads
sensibly and back off on a 429-shaped error. Query depth and field-count guards exist too; the
operations in the skill's examples/ are well inside them.
Introspection is not available. The bundled references/schema.graphql is not a
convenience — it is the only schema source there is. It is a generated snapshot carrying the
surfaces a scaffolded app works with, names and types only: no descriptions and no deprecated
members. Two things follow. A name you cannot find is not necessarily a name you got wrong —
it may be one the bundle deliberately does not carry, and a name that is not in it is a name to
treat as unavailable here. And which error codes a given mutation can return is not in the file
at all, so take them from the module guide covering that surface and switch on the raw
extensions.code string with a fallback branch rather than on an exhaustive enum.
Validate every operation against that snapshot before running it live; that is what
scripts/validate-operations.mjs is for, and with no arguments it validates this skill's
examples and every sibling chatfuel-* skill's. The SDL and its possible-types.json are not
checked into the skill directory: one copy lives in content/schema, and the wizard writes both
into the installed core skill's references/ so the file every skill points at is the file the
codegen reads.
An app the wizard wrote refuses a document it does not ship. Its proxy checks every request
against the operation documents the app exports and answers 403 OperationNotInRegistry for
anything else — including a field added to an existing operation, since that is a different
document. So an operation of your own has to be exported the way the others are, not merely
written somewhere the client can reach.
Three fields will bite whoever writes the first query. Always select __typename on
Contact and Conversation — fields come back missing otherwise, and selecting it on every
interface and union is cheap insurance. clientId on a message must be a fresh UUID, unique
across all clients of the account, because everything writing to that account merges by it and
a collision corrupts both lists; Message.id is nullable and clientId is the reliable key.
And never query GetDefinedErrorCodes: it exists only to union the error enum across subgraphs
and always errors when called.
Pagination has three specific liars. Bot.whatsAppTemplates accepts first, after and
before and returns the full list regardless. Seven fields have no pagination at all —
Bot.goodsCatalog, Bot.specialists, Bot.contactScopes, Bot.members, Bot.invites,
Workspace.bots and CurrentUserAccount.workspaces. And an after cursor that has fallen out
of the result window answers an invalid-cursor error; the recovery is refetching from the start.
The socket guarantees "from now on" and nothing more. One lazy connection per token, a ping
about every ten seconds with a dead socket declared at fifteen, exponential backoff with jitter,
and six close codes not to retry on (4400, 4401, 4403, 4406, 4409, 4429). On every
reconnect, refetch the queries behind every live view: the server does not replay what was
missed while you were away.
Strip __typename from variables. Round-tripping a fetched object into a mutation input
without recursively removing it is rejected — the server refuses unknown input fields.
The Chatfuel API
The API itself: transport and auth, the error codes, pagination, files and tasks, and the traps.
The token boundary
What the mandatory proxy becomes in a deployed app, and why the browser never holds the Chatfuel token.
Every module
The fourteen the wizard can install, and what each surface built on this one adds to the app.
Your agent's notes
The wizard installs .claude/skills/chatfuel-core/ (Codex: .agents/skills/chatfuel-core/) — the full SDL, the possible-types map, transport-auth.md, cors-proxy.md, pagination.md, files-tasks.md, platform-links.md, misc.md, gotchas.md and the operation validator.
Admin
The operator's panel over the whole Chatfuel account behind the token — reached by a password in the server environment, never by an identity, and never from the rail.
Guides
Twelve procedures end to end — changing what the app is, giving it to other people, putting it somewhere, and running the wizard again over an app that already exists.