Skip to content
ChatfuelSDK
Concepts

The auth gate

Behind the proxy sits one Chatfuel account holding every customer's bot, so two fences decide what a request may name — and both fail closed.

Behind the proxy there is one Chatfuel account, and every bot the deployment serves lives in it. The token that reaches that account reaches all of them. So the question "may this request go through?" is not the same as "may this request name that bot?", and the proxy answers both — separately, and in that order.

Two fences, two questions

QuestionWhat answers itWhat it asks
May this deployment touch that bot at all?the workspace fence, always onChatfuel, with the master token
Does this signed-in person own that bot?the auth gate, only with the auth moduleyour Supabase project, with the caller's own session

They are not layers of the same check. The fence is a property of the deployment and applies with no auth module installed at all; the gate is a property of the request. Where the gate applies, it wins — its answer is narrower, because it is about one person rather than one account.

Without the auth module: the deployment fence

A Chatfuel account is a tree — workspaces, each holding bots — and the app lets a person move between the workspaces that account owns. So the fence is asked for rather than configured: the proxy queries the account's workspaces and their bots with the master token and caches the answer, defaulting to 60 seconds. A bot created in the Chatfuel dashboard five minutes ago belongs to the same account as the rest, and nobody has to redeploy for it to work.

The two failure modes are deliberately different. If a previous answer exists and a refresh fails, that answer is served on for a short retry window — an upstream blip must not lock out an app whose bots have not changed. If nothing is known yet and the query fails, the request is refused with ProxyFenceUnavailable, because forwarding a bot id nobody has vouched for is the one thing worse than an error.

With it: the session gate

Set both Supabase variables and every request must additionally carry the caller's session — a bearer token on HTTP, an authToken in the WebSocket's connection_init — and every bot that request names must belong to a workspace that session is a member of.

The proxy verifies nothing itself. It reads the JWT's unverified expiry, so an expired token is refused with zero network, and otherwise asks your own Supabase project which bots this caller may open. PostgREST checks the signature; a bad or expired token comes back as its 401.

The answers map onto codes a module can branch on. No session, or one Supabase rejects, is AuthSessionRequired. A signed-in caller with no workspace yet that names a bot is AuthTenantForbidden — a session-level state the app fixes by provisioning, not a permission failure. A bot that belongs to somebody else is BotNotAllowed. Supabase unreachable is ProxyAuthUnavailable, never a silent pass.

Both fail closed

Half a configuration is not a configuration. Set one of the two Supabase variables without the other and every proxied request answers 500 ProxyAuthMisconfigured — the proxy refuses to guess which half you meant, because both guesses are wrong in a way you would only find out about later. The same instinct runs through the fence: unknown means refuse.

That is the behaviour to expect from a deployment that is misconfigured rather than broken. It looks like total failure, on purpose. Picking a host covers reading the gate's state off the startup line and the health route before anything else.

Answers are cached for 30 seconds, deliberately

The gate caches by sha256 of the token, for 30 seconds, bounded by the token's own expiry, and it caches positive answers only. So a just-invited colleague is let in on their next request rather than after a redeploy, and a refusal is never remembered. The raw token is never stored.

The window is a real one, and worth knowing rather than hiding: a member removed from a workspace can keep making HTTP calls for up to 30 seconds. WebSockets are stricter in one direction and looser in another — the socket is gated before it is opened, so an unauthenticated socket never reaches Chatfuel at all, but rotation is checked at connect only. Somebody removed mid-session keeps their open socket until it drops.

One tenant, many bots

The live model is one workspace per account with many Chatfuel bots inside it. Somebody signs up, the server creates their first bot with the deployment's master token, and they can add, rename and delete more. Colleagues arrive by invite into the same workspace, and which bots each of them may open is granted per person — owners and admins reach every bot in the workspace by role, and carry no grant rows at all.

An earlier version of the schema gave a workspace exactly one bot. That has not been true since the 0002_multi_bot migration, and any wording you find that still says "one tenant per deployed bot" is stale.

Two constraints carry more weight than they look:

  • cf_tenants.created_by is unique. That single index is what makes sign-up safe — two tabs or a retried request cannot open two workspaces, and therefore cannot mint two accounts' worth of bots.
  • Adding a bot is two steps on purpose. The database decides whether the caller may, and reserves a row; then the server creates the bot in Chatfuel and attaches it with the service key. If either half fails the other is undone, so neither side is left holding what the other forgot.

Nothing caps how many bots an account may create. The ceiling is the Chatfuel workspace's own plan, and a full one answers WorkspaceFull — to sign-ups and to "new bot" alike.

What the fence does not cover

Be precise about what this buys, because the gap is not obvious.

The fence is exact for anything that names a bot — the variable, the root field, on HTTP, REST and every WebSocket subscribe frame. Naming somebody else's bot never reaches Chatfuel.

It does not cover operations that name a flow, a contact or a conversation instead. Those ids are issued per bot and never listed across bots, but the proxy does not resolve them to a bot and Chatfuel will not either — behind the master token every bot belongs to the same account. Treat resource ids as unguessable, not as a boundary.

And a request that names no bot at all passes every fence. That is why, with the gate on, the fields that would describe the whole Chatfuel account rather than one workspace are refused outright with AccountScopeBlocked — listing the deployment's own bots would hand one customer the list of all the others.

For the variables that turn any of this on, see environment variables.

On this page