Skip to content
ChatfuelSDK
Deployment

Picking a host

Which host to run your app on, who may reach it once it is up, and the two rules that hold whichever one you pick.

Nothing about the app is Vercel-specific except vercel.json and api/chatfuel.ts; delete both and you have a working Node app. So the host you pick decides who manages TLS, whether you can serve from a sub-path and how long a WebSocket stays open — not what the code is. What it does not decide is who may reach the deployment, and that question comes before the host.

Picking a host

VercelYour own serverDocker
Commandnpm run deploynpm run build && npm startdocker build + docker run
Needs an accountVercelnonenone
TLS, domainmanagedyoursyours
Sub-path (/app/)no, root onlyyesyes
WebSocket lifetimecapped by the function duration (300 s on Hobby); the client reconnectsunboundedunbounded
Where the env livesthe Vercel project (.env is never uploaded)your process--env-file and build args

All three run the same proxy source, so the auth gate, the fences and the error codes are identical between them.

Who may reach it

Without the auth module the deployment has no sign-in and no callers of its own: the proxy admits every request that arrives. That's the right shape for a tool your own team uses — one Chatfuel account behind it, the app as its interface — and it means the URL is the credential. Anyone who reaches it acts as the deployment's Chatfuel account, inside whatever bot fence it was given: reading conversations and contacts, sending messages as the bot, changing flows and automations. The token itself never leaves the server, so it can't be lifted from the page — but every request the token can make, an anonymous visitor can make through the proxy.

So an open deployment stays off the public internet. Keep it on loopback, or put it behind something that decides who gets through:

  • a VPN, a Tailscale or WireGuard network, or an office IP allowlist;
  • your host's own access control — Vercel's deployment protection, nginx auth_basic, an identity-aware proxy such as Cloudflare Access;
  • localhost only, which is what npm run dev gives you.

If the app is meant to be reached from the open internet — by customers, or by staff signing in as themselves — it needs the auth module. That's what makes callers a concept at all: each one signs in against your Supabase project, and the proxy resolves the bots that caller may open before it forwards anything. Adding it later is npx @chatfuel/wizard --embed; it isn't a setting to flip in production. The auth gate is the concept, Accounts is the module.

The two shapes the server refuses to serve

Both are the same fact — the master token answering whoever asks — and a startup warning is the wrong instrument for it: a boot line is read once, by the person who already knows, and never again by the deployment that outlives them. So npm start prints REFUSING TO SERVE and never binds, vite dev --host stops before it mounts the proxy, and the Vercel function answers 503 ProxyRefusedToServe to everything but the health route.

  • Open mode on a host that is not loopback. No gate, and a socket somebody other than you can reach: 0.0.0.0 (the default), a LAN address, a name — anything other than 127.0.0.1, localhost and ::1. Three ways out: install the auth module (npx @chatfuel/wizard --embed), bind loopback (HOST=127.0.0.1), or — if answering strangers under one token is what this deployment is for, because something in front of it authenticates — say so with CHATFUEL_OPEN_PROXY=1. The last one is a signature, not a fix: the deployment prints one line saying it was meant, and whatever authenticates in front of it is the whole defence.
  • ALLOWED_ORIGINS=* on a host that is not loopback, gate or no gate. The proxy answers any origin with credentials, so any page a visitor opens may script it out of their browser — under the master token with the gate off, as the visitor with it on. There's no acknowledgement for this one; CHATFUEL_OPEN_PROXY doesn't cover it, because naming the origins that serve your app costs one variable.

Both variables, with the rest of what decides who may call, are on Environment variables.

Where the mode is named

The mode isn't a guess: the server prints it on its startup line, and that line is the only place it's named. /chatfuel/healthz — the same path on the bundled Node server, the Vite dev server and the Vercel function — answers {"ok":true} with 200, or {"ok":false} with 503 when the configuration has a problem, and nothing else. It's open to anyone who reaches the deployment, and an unauthenticated caller has no business learning whether the gate is off before trying a request. It sits inside the proxy's prefix because on Vercel vercel.json sends everything outside /chatfuel/* to index.html: a bare /healthz there is the SPA answering 200 with HTML. A page back instead of JSON means you asked the wrong path.

What the startup log says unprompted

Beside the listening line, npm start writes a warning to stderr for each shape below. None of them refuses anything; each is invisible from inside a running deployment, and the boot is the one moment somebody is reading:

  • CHATFUEL_OPEN_PROXY=1 on a reachable host — the acknowledged intent, restated at every boot.
  • Sign-ups open on the Supabase project, and separately sign-ups open with email confirmation off. The server asks the project for these at startup — /auth/v1/settings, the unauthenticated document the browser SDK reads anyway — doesn't wait for the answer, and stays quiet if it gets none. Closing sign-ups and turning confirmation on is in Multi-tenant SaaS.

/admin is a second door

The admin panel is not behind the auth gate — it can't be, because it reads and changes the whole Chatfuel account behind CHATFUEL_TOKEN, and an open deployment has no accounts to gate it with. ADMIN_PASSWORD is the only thing in front of it: no second factor, no IP allowlist, no lockout that outlives a process, and nobody to appeal to if it's guessed. Nothing in the template protects /admin for you. Sixteen characters is the floor the proxy refuses to start below, and a floor is not a recommendation.

The wrong-answer counter in front of that door is kept in your database (cf_admin_attempts) when the auth module is installed, so every instance of the proxy shares one count and a run of guesses spread across them is counted once. Installing auth isn't enough on its own: that shared counter is written with SUPABASE_SERVICE_ROLE_KEY, which the proxy treats as optional, so with the module on and the key unset a stateless host is back to a best-effort count in one process's memory.

So on a host that answers from whichever instance happens to be warm — Vercel, or anything like it — set the key. If you can't, keep the panel off that host: leave ADMIN_PASSWORD unset there and run the panel somewhere you control, or put your own authentication in front of it. The admin panel is the procedure.

The one rule every host must obey

An unknown path must answer with index.html. The router is path-based, so /deals/board is a real address. A host that 404s it produces a failure that looks like a routing bug and is not one: a reload of that URL fails while clicking through to the same page works fine.

  • Vercel — the last rewrite in vercel.json.
  • nginx — try_files $uri /index.html;
  • Static hosts — their SPA-fallback, or "rewrite everything to index.html".
  • The bundled Node server does it already.

The rule that catches everyone once

VITE_* variables are compiled into the browser bundle at build time. Everything else is read by the server at runtime. A deployment where those two disagree does not fail at the mismatch; it fails later, at the gate:

  • client built with Supabase, server started without it → the sign-in screen renders and every request comes back ProxyAuthMisconfigured or AuthSessionRequired;
  • server gated, client built without it → nobody is ever asked to sign in, and the server rejects everything.

Neither is fixable at runtime. Rebuild with the same values the server is running with, and read the startup line for the mode — the health route says only whether the server is up.

Deploying without the wizard

npm run deploy is a convenience, not a dependency: it is scripts/deploy-vercel.mjs inside your own project, driving the Vercel CLI. Any pipeline that builds the client, builds the server and sets the environment produces the same result, and any CI that can run npm run build can deploy this app.

On this page