# The proxy

> Every route the proxy mounts, which of them exist only when a module or a variable does, the options that move them, and the three hosts that serve the same core.

Page: https://sdk.chatfuel.com/docs/reference/proxy-routes
Markdown: https://sdk.chatfuel.com/docs/reference/proxy-routes.md

One core is assembled from per-concern modules and imported from one place by all three hosts, so
the route table is the same in dev, on a Node server and on Vercel.

## The routes [#the-routes]

Paths are the defaults; every one of them is an option.

| Route                               | Mounted                            | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /chatfuel/graphql`            | Always                             | Forwards the body to `{upstream}/graphql` with `authorization: Bearer <CHATFUEL_TOKEN>`. Every bot the body names — the `botID` variable, and the argument of a root `bot(id: $x)` field, read out of the query text rather than trusted to a convention — is checked against the fence first. Any other method is a 405 with an `allow` header and a bare body: a wrong method is a caller-side programming error, not an application answer. |
| `WS /chatfuel/graphql`              | Always                             | The `graphql-transport-ws` relay. Same path as the POST route; the Upgrade disambiguates. An upgrade on any other path is a no-op, which is what lets Vite's own HMR socket share the server.                                                                                                                                                                                                                                                  |
| `/chatfuel/api/*`                   | Always                             | Passthrough to `{upstream}/api/*` — the REST file uploads — with the same token injection. Any method; a `botID` query parameter is fenced the same way a GraphQL variable is.                                                                                                                                                                                                                                                                 |
| `POST /chatfuel/auth/provision`     | Gate on **and** a service-role key | The second half of signing up: the account's workspace and its bots, adding the first bot when it has none. Idempotent — somebody who already has a workspace gets it back. Without `CHATFUEL_WORKSPACE_ID` it stops rather than creating a bot that bills nowhere.                                                                                                                                                                            |
| `POST /chatfuel/auth/bots`          | Same                               | Another bot for the caller's workspace. Body `{name}`.                                                                                                                                                                                                                                                                                                                                                                                         |
| `PATCH /chatfuel/auth/bots/<id>`    | Same                               | Renames it, in the database first and then in Chatfuel. Body `{name}`.                                                                                                                                                                                                                                                                                                                                                                         |
| `DELETE /chatfuel/auth/bots/<id>`   | Same                               | Deletes it, in Chatfuel first and then in the database.                                                                                                                                                                                                                                                                                                                                                                                        |
| `POST /chatfuel/auth/recovery-link` | Gate on **and** a service-role key | Body `{email}` from an owner or admin; answers `{ tokenHash, url }` so an operator can reset a member's password before custom SMTP exists.                                                                                                                                                                                                                                                                                                    |
| `/chatfuel/admin/*`                 | `ADMIN_PASSWORD` is set            | The operator panel — its own table below.                                                                                                                                                                                                                                                                                                                                                                                                      |
| `/chatfuel/publishing/*`            | Gate on **and** a service-role key | The Instagram publish queue — its own table below.                                                                                                                                                                                                                                                                                                                                                                                             |

`POST /chatfuel/auth/provision` and the three `bots` routes are *mounted* on the gate and the
service-role key; `CHATFUEL_WORKSPACE_ID` is checked inside the handler, so a deployment missing it
gets an answer that names the variable rather than a 404.

## What decides whether a route exists at all [#what-decides-whether-a-route-exists-at-all]

| Resolved flag          | True when                                                          | Routes it mounts                                                              |
| ---------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `provisionRoute`       | `authMode === 'on'` and a service-role key is available            | `/chatfuel/auth/provision`, `/chatfuel/auth/bots`, `/chatfuel/auth/bots/<id>` |
| `recoveryLinkRoute`    | The same condition                                                 | `/chatfuel/auth/recovery-link`                                                |
| `publishingQueueRoute` | The same condition                                                 | Everything under `/chatfuel/publishing`                                       |
| `adminRoute`           | `adminMode` is not `off` — that is, `ADMIN_PASSWORD` is set at all | Everything under `/chatfuel/admin`                                            |

A route that is not mounted is not *claimed*: the dispatcher returns false, the host answers its own
404, and that 404 is the signal. The app reads a 404 on the publishing routes as "this deployment
keeps its queue in the browser" and offers no schedule control. A deployment that never asked for a
panel answers 404 on `/chatfuel/admin` and the app reads that as "no panel".

The one exception is a password that is set but shorter than 16 characters: `adminMode` becomes
`misconfigured`, the routes stay claimed, and they answer 500 — a panel that quietly is not there
teaches the operator nothing.

<Callout type="warn">
  The publish queue has a second, quieter half. With the routes mounted but `PUBLISHING_SECRET`
  unset, the queue still stores posts and never fires them, and the scheduler's callback route
  refuses every request.
</Callout>

## Under `/chatfuel/admin` [#under-chatfueladmin]

Every path except `/session` requires a signed `cf_admin` cookie **and** an `x-cf-admin` request
header — a form posted from another origin can send a cookie but cannot set a header. The cookie is
minted from `ADMIN_PASSWORD`, lasts 12 hours, and is the whole authorization: this module never calls
`admitRequest`, never consults the auth gate and never applies the workspace fence, because the
panel's purpose is the account-wide view those exist to withhold.

| Route                             | Methods                  | What it does                                                                          |
| --------------------------------- | ------------------------ | ------------------------------------------------------------------------------------- |
| `/chatfuel/admin/session`         | `GET`, `POST`, `DELETE`  | Read the session, unlock with the password, sign out.                                 |
| `/chatfuel/admin/overview`        | `GET`                    | The account behind the master token: its workspaces and their bots.                   |
| `/chatfuel/admin/health`          | `GET`                    | Whether the token still works and what this deployment is missing.                    |
| `/chatfuel/admin/tenants`         | `GET`                    | The app's own accounts, where the `auth` module is installed.                         |
| `/chatfuel/admin/grants`          | `POST`, `DELETE`         | Hand one bot to one of the app's accounts, and take it back.                          |
| `/chatfuel/admin/bots`            | `POST`                   | Create a bot.                                                                         |
| `/chatfuel/admin/bots/<id>`       | `GET`, `PATCH`, `DELETE` | One bot's details, rename, delete.                                                    |
| `/chatfuel/admin/workspaces/<id>` | `GET`                    | One workspace. An id that is not `[A-Za-z0-9_-]{1,64}` is a 404 before anything runs. |

Anything else under the prefix is `404 AdminRouteNotFound`.

## Under `/chatfuel/publishing` [#under-chatfuelpublishing]

| Route                              | Methods           | What it does                                                                                                          |
| ---------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| `/chatfuel/publishing/config`      | `GET`             | Whether this deployment can schedule at all.                                                                          |
| `/chatfuel/publishing/register`    | `POST`            | Records where this app answers, so the scheduler knows where to knock. Owner or admin only.                           |
| `/chatfuel/publishing/posts`       | `GET`, `POST`     | The bot's queued posts, and a new one. `?botID=` is required and fenced.                                              |
| `/chatfuel/publishing/posts/<id>`  | `PATCH`, `DELETE` | Edit or drop one. The id must be a UUID, or it names no post and the answer is a 404.                                 |
| `/chatfuel/publishing/media`       | `POST`, `DELETE`  | Durable storage for what a scheduled post is made of — the platform's own uploads expire two hours after they arrive. |
| `/chatfuel/publishing/publish-due` | `POST`            | The scheduler's callback. Authorised by `x-chatfuel-publish-key` against the shared secret, never by a session.       |

## The options [#the-options]

`createChatfuelProxy(options, env)` takes these; each falls back to the env bag, then to a default.

| Option             | Default                                                | What it changes                                                                                                                                                                                                                                                                                      |
| ------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `upstream`         | `CHATFUEL_API_BASE`, then `https://panel.chatfuel.com` | Where every forwarded call goes. Trailing slashes are trimmed.                                                                                                                                                                                                                                       |
| `httpPath`         | `/chatfuel/graphql`                                    | The GraphQL POST path.                                                                                                                                                                                                                                                                               |
| `wsPath`           | Same as `httpPath`                                     | The upgrade path. POST and Upgrade disambiguate, so they can share.                                                                                                                                                                                                                                  |
| `apiPath`          | `/chatfuel/api`                                        | Forwarded to `{upstream}/api`.                                                                                                                                                                                                                                                                       |
| `authPath`         | `/chatfuel/auth`                                       | Prefix for `provision`, `bots` and `recovery-link`.                                                                                                                                                                                                                                                  |
| `publishingPath`   | `/chatfuel/publishing`                                 | Prefix for the publish queue.                                                                                                                                                                                                                                                                        |
| `adminPath`        | `/chatfuel/admin`                                      | Prefix for the operator panel.                                                                                                                                                                                                                                                                       |
| `adminPassword`    | `ADMIN_PASSWORD`                                       | Unset unmounts the panel; shorter than 16 characters makes it answer 500.                                                                                                                                                                                                                            |
| `publishingSecret` | `PUBLISHING_SECRET`                                    | The secret the scheduler and this server share. Without it nothing is scheduled.                                                                                                                                                                                                                     |
| `bypassSecret`     | `VERCEL_AUTOMATION_BYPASS_SECRET`                      | Recorded at registration so the scheduler's callback is not turned away at the edge before any of this code runs.                                                                                                                                                                                    |
| `publicUrl`        | `PUBLIC_URL`                                           | Where this deployment answers from the outside. Anything that is not an absolute `http(s)` URL is dropped rather than patched up — this value decides where a credential is posted every minute, and deriving it from a request means the requester chooses it.                                      |
| `tokenEnv`         | `'CHATFUEL_TOKEN'`                                     | Which variable holds the Chatfuel token.                                                                                                                                                                                                                                                             |
| `token`            | `env[tokenEnv]`                                        | Explicit override, for tests. A value containing whitespace is treated as missing.                                                                                                                                                                                                                   |
| `workspaceId`      | `CHATFUEL_WORKSPACE_ID`                                | The Chatfuel workspace provisioned bots are created in — the billing container.                                                                                                                                                                                                                      |
| `timeoutMs`        | `30_000`                                               | Upstream HTTP budget.                                                                                                                                                                                                                                                                                |
| `slowTimeoutMs`    | `290_000`                                              | The budget for the four `instagramAccountPublish*` fields, which block while Instagram transcodes. Separate rather than a raised default, so a dead upstream is still felt in thirty seconds everywhere else, and a little under five minutes because that is where a serverless function is killed. |
| `allowedBotIds`    | Unset — the request-time fence                         | A list freezes the fence at startup; `'any'` turns it off entirely.                                                                                                                                                                                                                                  |
| `fence`            | —                                                      | `ttlMs` (60 s), `retryMs` (5 s), `timeoutMs` (10 s) and test injection for the request-time fence. It exists only when a token does.                                                                                                                                                                 |
| `auth`             | Resolved from env                                      | An object forces the gate on with those values; `false` never gates.                                                                                                                                                                                                                                 |
| `wsInitTimeoutMs`  | `5_000`                                                | How long the relay waits for the browser's `connection_init` before closing.                                                                                                                                                                                                                         |

`auth` resolved from env has three outcomes: both `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY`
set is `on`, neither is `off`, and exactly one is `misconfigured` — which fails closed, so every
proxied request answers 500. See [the auth gate](/docs/concepts/auth-gate) for what the gate then
does, and [the environment](/docs/reference/environment) for where the variables come from.

## The three hosts [#the-three-hosts]

| Host            | File                         | Serves                                                      | What differs                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------- | ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Vite dev server | `vite.ts`                    | The proxy, mounted as middleware plus an `upgrade` listener | The only file in the package that imports `vite`. It builds the env bag as `loadEnv(mode, envDir, '')` — an empty prefix, so unprefixed secrets are read from `.env` without ever reaching `import.meta.env` — then lets `process.env` win. A configuration problem is logged, never fatal: requests answer with a synthetic envelope so the problem is visible in the app.                                                                                                                                               |
| Node server     | `server.ts`                  | `/healthz`, then the proxy, then the built `dist/`          | Reads `process.env` only. Static serving is GET/HEAD, contained inside `distDir` before any filesystem call, `immutable` under `/assets/`, `no-cache` for `index.html`, and an unknown path that is not asset-shaped falls back to `index.html`. `basePath` mounts all of it under a sub-path. An upgrade that is not the relay's path is destroyed. Not re-exported from the package entry — importing it by path is what keeps `vite` out of the server bundle.                                                         |
| Vercel function | `apps/shell/api/chatfuel.ts` | The proxy only — Vercel serves `dist/` itself               | The module's default export is an `http.Server`, which is what makes incoming WebSocket upgrades work. `vercel.json` rewrites `/chatfuel/:cfpath*` to `/api/chatfuel`, and `restoreUrl` turns `?cfpath=auth/provision` back into `/chatfuel/auth/provision` before the core sees it, so the route table needs no per-host variant. Env is read at module scope, once per cold start. `/chatfuel/healthz` answers 200 or 503 and lists the configuration problems by name. An unclaimed path is a `404 NotFound` envelope. |

A rewrite straight to `panel.chatfuel.com` would be cheaper and cannot work: a rewrite forwards the
browser's headers and has no way to add one, so the token would have to be sent by the client — the
one thing the proxy exists to prevent. The `headers` block in `vercel.json` sets response headers,
not request ones.

## Nothing the client sent is forwarded [#nothing-the-client-sent-is-forwarded]

Outgoing headers are built from scratch rather than merged, so the browser's `Authorization` and its
cookies are stripped by construction:

* The GraphQL forward sends exactly `content-type: application/json` and
  `authorization: Bearer <token>`.
* The REST passthrough sends `authorization` plus the request's `content-type` when there is one,
  because a multipart upload needs its boundary. Nothing else, cookies included, travels.
* The WebSocket relay waits for the browser's `connection_init`, gates it, and then sends *its own*
  `connection_init {authToken: "Bearer …"}` upstream. The browser's payload — the user's session JWT
  — is consumed by the gate and never leaves the server.

Coming back the other way, the upstream status and body pass through untouched, because a GraphQL
refusal arrives as an envelope under HTTP 200 and rewriting it would lose the answer. The proxy's own
refusals are `no-store`: a cached 403 would keep saying "not your bot" about a bot that has since
become the caller's. Which codes those are is on [the errors page](/docs/reference/errors).
