# Ceilings and fences

> What a caller past the gate may actually do — the per-process ceilings, the tenant bucket, the bot and resource fences, the operation allowlist and the document registry.

Page: https://sdk.chatfuel.com/docs/concepts/fences
Markdown: https://sdk.chatfuel.com/docs/concepts/fences.md

The gate answers one question: which bots may this session touch. It says nothing about how much of
the deployment that session may spend, which operations exist at all, or which ids inside a bot it
may name — and behind the proxy there is still one master token reaching one Chatfuel account. Most
of the API does not name a bot at all, so most requests pass every fence the gate feeds.

So there is a second set of answers, and none of them is about identity.

| What it bounds               | What it refuses                                                       | Code                                                                        |
| ---------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| One process's in-flight work | more uploads, GraphQL requests or sockets than this instance holds    | `ProxyBusy`, a refused upgrade                                              |
| One tenant's share           | more requests a minute, or more live sockets, than one workspace gets | `TenantBusy`, close `4429`                                                  |
| The bot fence                | a bot that is not the caller's                                        | `BotNotAllowed`                                                             |
| The account denylists        | reading or reshaping the Chatfuel account behind the deployment       | `AccountOperationBlocked`, `AccountScopeBlocked`, `AccountStructureBlocked` |
| The resource fence           | an id inside a bot that was handed out under a different one          | `ResourceNotAllowed`                                                        |
| The operation allowlist      | a root field no shipped module sends                                  | `OperationNotAllowed`                                                       |
| The document registry        | a document this app does not ship                                     | `OperationNotInRegistry`                                                    |

Every default, variable and status behind that table is on [environment
variables](/docs/deploy/environments), [the proxy](/docs/reference/proxy-routes) and
[errors](/docs/reference/errors).

## What one process will spend [#what-one-process-will-spend]

A ceiling on one request is not a ceiling on the process: a 25 MiB upload times *N* costs *N* times
that, and *N* is whatever the caller opened. So the number in flight is bounded too — separately for
uploads and for GraphQL, which the whole app talks through and gets the larger number. A batch is
counted again on its own, because upstream it is one POST and here it is one slot and one token from
the tenant's minute.

Sockets are the same shape, plus a much smaller ceiling on the sockets *waiting to be admitted*: one
that has shown nothing still holds a place for as long as the init timeout allows, and a small
budget there keeps the admitted sockets running while the unadmitted contend for it.

## The tenant's share [#the-tenants-share]

Those ceilings are the deployment's, which is the whole of the protection while it serves one
customer. With the gate on it serves many, and a global ceiling is one a single tenant can spend on
everybody else's behalf: 256 sockets is 256 sockets whether they come from 256 customers or from one
script. So the same shape again, keyed by tenant — and neither the bucket nor the socket count is a
security boundary, because the fences are that.

The key is the fence itself rather than the session: a tenant with four tabs is still one tenant,
and the bots they may touch is the only stable name the proxy has for them — which is also why the
bucket is felt only where there is a fence to key by. Accounts that have signed up but hold no bot
yet all hash to the same nothing, so they share one bucket at a fraction of a tenant's, sized for
what they can legitimately do: finish signing up. All of it lives in one process, so what it bounds
is one instance's share of a noisy neighbour; the caps that must hold everywhere live in SQL.

## The bot fence, and the resource fence behind it [#the-bot-fence-and-the-resource-fence-behind-it]

The bot fence is exact for anything that names a bot, and that is where it stops. An operation that
names only the *account* passes every fence there is, so those are refused by name instead — [the
auth gate](/docs/concepts/auth-gate) has that half.

The other half is ids inside a bot. Upstream authorizes the token, not the bot an id belongs to, so
nothing there will resolve one for the proxy. The proxy keeps its own memory instead: every such id
reaches a browser exactly once, inside an answer to a request the bot fence had already checked, and
the fence writes down which bot it was handed out under. A later request naming an id known to be
another bot's is refused before the master token sees it.

`bound`, the default with the gate on, refuses only what it knows to be foreign, so it cannot turn a
legitimate request away. `strict` refuses the unknown too. `off` is the default without the gate,
where there is one tenant and nobody to be foreign to. Two rules make the memory hard to poison: an
id the request itself carried is never learned from that request's answer, and an id seen under a
second bot is marked *shared* rather than re-owned. A shared id refuses nobody, so the attack costs
a refusal at worst and never a read.

That memory is one process's. A shared store on the deployment's own Supabase gives it a floor,
which is what makes `bound` survive a restart and `strict` safe on more than one instance — and the
table is service-role only, because a caller who could read it could ask which bot an id belongs to.

## The operations this app sends [#the-operations-this-app-sends]

The fences above each name something dangerous. The allowlist asks the opposite question: of the
thousands of fields in Chatfuel's schema, which does this app actually send? 353 — and every one was
already written down, because the modules' own operation documents are what codegen reads. The list
is generated from them and the repository's validator re-derives it, so a module that gains an
operation cannot ship a feature the proxy refuses.

It is on with the gate and off without it, where the caller is the deployer and refusing them a
field of their own schema protects nobody. Turning it off *behind* the gate is one variable away
from the widest opening the proxy has, so it asks to be said twice: the value, and an
acknowledgement beside it. Said once, it is ignored and the startup line says so.

## The documents this app ships [#the-documents-this-app-ships]

The narrowest check, and the first one asked. A name check cannot tell one shape of `bot { … }` from
another: `CurrentUser`, and a `CurrentUser` with `apiToken` added to it, have the same root field, so
the allowlist sees one operation where the registry sees two.

So the app hands the proxy its own generated namespaces — a barrel the wizard writes, one
`import * as` per module and never `export *`, which would silently drop a name two namespaces
share. A document is admitted by its exact text, and failing that by a hash of that text with
whitespace, commas and comments stripped, so a bundler that moved a newline is not a production
refusal; a field added, an alias or a different operation name is a different document. What goes
upstream on a match is the app's own text and operation name, read off the document rather than off
the request.

A host that passes none keeps serving with the check off, so an app scaffolded before the barrel
existed still boots — and says so on the startup line, because silence there would read exactly like
a registry that is holding.

## Where it is all announced [#where-it-is-all-announced]

Every host prints its resolved configuration once, on one line: the gate, both fences and whether
the resource store is shared, the size of the allowlist, the number of documents in the registry,
whether the admin panel is mounted, and whether any origin may call. A deployment carrying something
left over from a demo says so at every boot.
