Skip to content
ChatfuelSDK
Deployment

Environment variables

Every variable the app reads, which side reads it, what it defaults to, and what it does.

A VITE_-prefixed secret is not a secret. Anything with that prefix is compiled into the browser bundle at build time; everything else is read by the server at runtime. That is why the Chatfuel token, the Supabase service-role key and the admin password have no prefix — Vite cannot bake them into the client even in principle.

Keep the build environment and the runtime environment identical. Where they differ, the failure surfaces at the gate rather than at the mismatch — see the rule that catches everyone once.

Always

NameSideDefaultWhat it is
CHATFUEL_TOKENserver—The dashboard token, from the token page. Read only by the proxy, never sent to the browser.
VITE_CHATFUEL_WORKSPACE_IDclient—The workspace the app opens on. Not a fence: every workspace the token's account owns is in the picker. Unused when the auth module is installed.
CHATFUEL_API_BASEserverhttps://panel.chatfuel.comUpstream API base. Change it only for a staging environment.
VITE_APP_NAMEclientChatfuel AppBrowser tab, top bar, sign-in screen.
VITE_APP_LOGOclientlogo.svgA file in public/, or an absolute URL. The tab icon itself is in index.html — the head is parsed before any of this is read.
VITE_CHATFUEL_DASHBOARD_URLclienthttps://panel.chatfuel.comWhere the empty states send someone to do what the app deliberately does not — create a workspace, create a bot, look at the bill. It does not default from CHATFUEL_API_BASE, which is unprefixed and so unreadable from the bundle.
PORTserver3000Port for npm start.
HOSTserver0.0.0.0Interface npm start binds. 127.0.0.1 makes the deployment this machine's own, which is what open mode may run on without asking for anything else. Vercel does not read it: a function has no interface to choose.

The auth module

VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY decide the gate: both set turns it on, neither is open mode, and one without the other is ProxyAuthMisconfigured — the proxy refuses every request rather than guessing which half you meant.

The rest is what the gate needs to be useful. Without SUPABASE_SERVICE_ROLE_KEY the server cannot register a bot it just created; without CHATFUEL_WORKSPACE_ID it has no Chatfuel workspace to create one in, and says so on start-up.

NameSideDefaultWhat it is
VITE_SUPABASE_URLclient and server—Project URL.
VITE_SUPABASE_ANON_KEYclient and server—Publishable key.
SUPABASE_SERVICE_ROLE_KEYserver—Secret key. Not optional under auth: it registers each bot the server creates, and it mounts the bot and password-recovery routes.
CHATFUEL_WORKSPACE_IDserver—The one Chatfuel workspace every customer's bot is created in — the one whose plan pays for them all, so its bot limit is the ceiling for the whole deployment. Unrelated to VITE_CHATFUEL_WORKSPACE_ID.
AUTH_RECOVERY_LINK_LOGserverofftrue or 1 lets /chatfuel/auth/recovery-link write the link it mints to the server log, which is that route's only delivery channel. Unset, the route mints nothing and answers 501.
SUPABASE_PROJECT_REFbookkeeping—The project's ref, written to .env when the wizard creates or links the project over an access token. Nothing reads it back; it is there so the id is on hand for the Supabase CLI.
CHATFUEL_BOT_CAP, CHATFUEL_BOT_TOTAL_CAPthe wizard runthe numbers in the migrationSubstituted into 0001_auth.sql as it is applied — the per-workspace ceiling and the whole-deployment one. Not written to .env: afterwards they are SQL in your own database. Anything that is not a plain number is left alone. The numbers are on Accounts.

The publishing module

Needed only for posts that go out unattended. Without them the module still composes and publishes on the spot; it offers no schedule. Turning scheduling on is an /admin action, so it needs ADMIN_PASSWORD as well as the gate.

NameSideDefaultWhat it is
PUBLISHING_SECRETserver—What the app and its database use to prove a request came from the other. The database stores only its sha256. Without it nothing is scheduled.
PUBLIC_URLserver—Where this deployment answers from outside, e.g. https://posts.example.com. Required to turn scheduling on and to issue a recovery link: each attaches a credential to the address it names, and nothing in the request is allowed to decide it, so unset is a refusal rather than a fallback.
VERCEL_AUTOMATION_BYPASS_SECRETserverset by VercelRecorded when the queue is registered, so the scheduler's callback is not turned away at the edge before any code runs.
PUBLISHING_MEDIA_BUCKETservercf-pub-mediaThe bucket in this deployment's own Supabase Storage that the proxy's media route stores into, so a post composed today is still published from a URL that resolves tomorrow. The app and the migration must name the same one; the wizard fills both from this value.
PUBLISHING_MEDIA_QUOTA_MBserver512How much of that bucket one bot may hold. Past it an upload answers 507 MediaQuotaExceeded and nothing is stored.
PUBLISHING_MEDIA_TTL_DAYSserver30How long a file no post of that bot refers to any more is kept. The next upload for that bot is what lets it go.

The admin module

NameSideDefaultWhat it is
ADMIN_PASSWORDserver—The only thing that opens /admin, which reads and changes the whole account behind CHATFUEL_TOKEN. At least sixteen characters, or the panel refuses to run and says why. Unset means the admin routes are not mounted at all. Changing it signs every open session out.
ADMIN_COOKIE_SALTserverderived from this deployment's own token and Supabase URLThe salt the admin session cookie is signed under. Set it to sign every open session out without changing the password, or when two deployments share a password and must not share cookies.
TRUST_FORWARDED_FORserveroff1 or true makes x-forwarded-for the key the wrong-password counter throttles on. Only behind an edge that writes that header itself; with nothing in front, the header is the caller's own text and a fresh value per attempt is the rate limit gone.

An admin session lasts two hours, and /admin is never in the navigation rail — the URL is the whole way in. The admin panel is the procedure.

Limits, and who may call

Every one of these is optional, and every default is the one a deployment gets by saying nothing. Ceilings and fences is what each of them is for.

NameSideDefaultWhat it is
ALLOWED_ORIGINSserversame-origin onlyOrigins besides the app's own whose pages may call the proxy, comma- or space-separated. Anything else answers 403 ProxyOriginForbidden before the route runs, and an upgrade is refused before a socket exists. * allows every origin with credentials, and on a host that is not loopback the server refuses to serve it.
ALLOWED_HOSTSserverthe hosts named by ALLOWED_ORIGINS and PUBLIC_URL, plus loopbackThe names this deployment answers to, as they arrive in Host — the answer to DNS rebinding. Anything else answers 403 ProxyHostForbidden. Set it for a tunnel or preview URL neither of those names, and prefer naming that host to *.
CHATFUEL_OPEN_PROXYserverunset1 says this deployment is meant to answer strangers under one token. It is a signature, not a fix, and it does not cover ALLOWED_ORIGINS='*'.
REST_MAX_CONCURRENTserver8How many /chatfuel/api/* uploads may be in flight at once. Each sits in memory while it is forwarded. Past it, 503 ProxyBusy.
GRAPHQL_MAX_CONCURRENTserver32How many GraphQL requests may be in flight at once. Higher than the upload ceiling because it is the route the whole app talks through.
GRAPHQL_MAX_BATCHserver25How many operations one GraphQL body may carry. Past it, 413 BatchTooLarge. The shell sends no batches at all.
WS_MAX_SOCKETSserver256How many browser WebSockets one process will hold. Each opens a second one upstream.
WS_PRE_AUTH_SOCKETSserver32How many of those may sit unadmitted at once — open, but not past the connection_init the gate reads. A small fraction of the total on purpose: what it protects is the sessions already admitted.
TENANT_REQUESTS_PER_MINUTEserver600How many requests one tenant may send per minute, as a token bucket that also holds a minute's worth as burst. Past it, 429 TenantBusy.
TENANT_MAX_SOCKETSserver8How many sockets one tenant may hold at once, out of WS_MAX_SOCKETS. Past it the socket closes 4429.
CHATFUEL_RESOURCE_FENCEserverbound with the gate, off withoutWhether a request naming a resource inside a bot — a flow, a block, a contact — is checked against the bot that id was handed out under. bound refuses an id it knows to be another tenant's; strict refuses one it has never seen too. An unrecognised value falls back to the default rather than to off.
CHATFUEL_RESOURCE_STOREserveron with the gate and a service-role keyWhether those bindings are shared through the deployment's own Supabase rather than one process's memory. It is what makes bound survive a restart and strict safe on more than one instance.
CHATFUEL_OPERATION_ALLOWLISTserveron with the gate, off withoutWhether an operation this app does not send is refused before any other question about it is asked. A root field that is not on the generated list answers 403 OperationNotAllowed.
CHATFUEL_OPERATION_ALLOWLIST_EXTRAserverunsetRoot fields to allow beside that list, comma-separated. It widens the fence by name, and a name says nothing about what the document under it selects.
CHATFUEL_OPERATION_ALLOWLIST_OFFserverunsetThe second half of turning the allowlist off behind the gate. CHATFUEL_OPERATION_ALLOWLIST=off alone is ignored and the startup line says it was ignored; 1 here is what makes it hold.

Serving from a sub-path

NameSideDefaultWhat it is
VITE_BASE_PATHclient/Build-time base, e.g. /app/. Absent is what makes the fallback to / happen — an empty string is not absent.
BASE_PATHserver/The same value at runtime. /app and /app/ are the same value to the server.

Both or neither, and not on Vercel — see Serving from a sub-path.

Behind a company proxy

Node's built-in fetch ignores these unless the process was started with a flag nobody types, and ws never reads them at all — so the proxy reads them itself, for HTTP and for the WebSocket relay. The wizard's own outbound calls go through the same code.

NameSideDefaultWhat it is
https_proxy, HTTPS_PROXYserverunsetThe proxy to send outbound requests through. First in the order that wins.
http_proxy, HTTP_PROXYserverunsetNext in that order.
all_proxy, ALL_PROXYserverunsetLast in that order.
no_proxy, NO_PROXYserverloopback onlyA comma list of hosts, each matching itself and its subdomains, or * for all of them.

Lowercase wins over uppercase, and an empty value means unset. Loopback is always exempt, whatever NO_PROXY says — a dev server on this machine is not the proxy's business.

What has to be reachable: panel.chatfuel.com always, registry.npmjs.org to install anything, api.vercel.com to deploy, api.github.com to push.

Where the values go on a deployment

npm run deploy pushes a fixed list of the names above into the Vercel project, production only — which names, which of them go up write-only, and what DEPLOY_PREVIEW_ENV=1 opts into are on the Vercel page. PORT, NO_PROXY and VERCEL_AUTOMATION_BYPASS_SECRET are never pushed: the first two belong to whatever runs the process, and Vercel sets the third itself.

On this page