# 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.

Page: https://sdk.chatfuel.com/docs/modules/core
Markdown: https://sdk.chatfuel.com/docs/modules/core.md

## What it is [#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 [#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 [#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 [#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](/docs/concepts/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&#x60;. **`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.

<Callout type="warn">
  **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.
</Callout>

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 [#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](/docs/deploy/environments).

## Permissions [#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 [#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.

<Cards>
  <Card title="The Chatfuel API" href="/docs/reference/chatfuel-api" description="The API itself: transport and auth, the error codes, pagination, files and tasks, and the traps." />

  <Card title="The token boundary" href="/docs/concepts/token-boundary" description="What the mandatory proxy becomes in a deployed app, and why the browser never holds the Chatfuel token." />

  <Card title="Every module" href="/docs/modules" description="The thirteen the wizard can install, and what each of the twelve surfaces built on this one adds to the app." />

  <Card title="Your agent's notes" href="/docs/concepts/agent-skills" description="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." />
</Cards>
