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 apps/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. 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 thing entirely and does not work for
GraphQL at all.
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.
That is the constraint the whole app is shaped around; the deployed form of it is on
the token boundary.
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 only about fourteen 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 gateway may answer 200 with an
errors[] array carrying an Unauthorized code from a subgraph, 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 subgraph failure reads "Failed to fetch from Subgraph 'x'" at
the top and carries 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
Twenty-five requests a second per token, authenticated. Query depth and field-count guards
exist too; the operations in the skill's examples/ are far below them.
Introspection is disabled in production. The bundled references/schema.graphql is not a
convenience — it is the only schema source there is. It is a copy of the dashboard's schema,
with the source commit named in its header, trimmed by script of the billing, Albato,
A/B-experiment and debug surfaces. Validate every operation against it 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.
Three fields will bite whoever writes the first query. Always select __typename on
Contact and Conversation — a known backend bug causes errors 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 the dashboard merges by it and a collision
corrupts both your list and theirs; 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 ignores all of them, always returning the full list. 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 thirteen the wizard can install, and what each of the twelve surfaces 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, 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
Four procedures end to end — adding a module to an app that exists, writing one of your own, retuning the design system, and mounting the modules inside somebody else's app.