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
| Vercel | Your own server | Docker | |
|---|---|---|---|
| Command | npm run deploy | npm run build && npm start | docker build + docker run |
| Needs an account | Vercel | none | none |
| TLS, domain | managed | yours | yours |
Sub-path (/app/) | no, root only | yes | yes |
| WebSocket lifetime | capped by the function duration (300 s on Hobby); the client reconnects | unbounded | unbounded |
| Where the env lives | the 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; localhostonly, which is whatnpm run devgives 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 than127.0.0.1,localhostand::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 withCHATFUEL_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_PROXYdoesn'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=1on 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
ProxyAuthMisconfiguredorAuthSessionRequired; - 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.
Vercel
One command: links the project, pushes the production environment, deploys, verifies.
Your own server
npm run build && npm start, what it refuses, and what to put in front of it.
Docker
The shipped Dockerfile, and the build-arg split that leaks secrets.
Environment variables
Every variable the app reads, which side reads it, its default, and what it does.