Open the operator panel
The panel reads and changes the whole Chatfuel account behind your token, so it opens on a password held next to that token and on nothing else. Installing it, the sixteen-character floor, and why /admin is the only way in.
The panel reads and changes every workspace and every bot behind this deployment's
CHATFUEL_TOKEN. That is more than any customer of yours should see, and it is not something
either identity already in the app can authorize. There is no second Chatfuel login to check —
the deployment holds one token for one account, and asking the operator to sign in to Chatfuel
from inside their own app would only prove they hold an account the token already speaks for.
A Supabase role is no better: the auth module is opt-in, a deployment without it has no accounts
at all, and one with it belongs to its customers.
So the credential is a password, held next to the token in the server-only half of the environment. Whoever can read that file already holds the token, so the panel grants nothing the credential did not.
Install the module
admin is opt-in, which means --yes leaves it out. Name it:
npx @chatfuel/wizard --modules admin,livechat,contactsThe wizard then has three ways to get a password, in order: the --admin-password flag, an
ADMIN_PASSWORD already in your environment, and a generated one. Generated is the default rather
than a prompt, because the thing being chosen is a secret nobody has to remember — it goes into
.env, which is where the panel reads it from, and a password typed by hand is mostly a weaker
one.
npx @chatfuel/wizard --modules admin --admin-password '<16 characters at least, no spaces>'A flag that cannot work is answered before the first question, not halfway through the run. And a
password the wizard invented is printed once, at the end, where you are still looking — it goes
into .env and into no other file, because the handoff note and the agent instructions are files
that get committed.
Put the password in the server environment
ADMIN_PASSWORD=<16 characters at least>Unprefixed, beside CHATFUEL_TOKEN. A VITE_ name would put it in the browser bundle.
Sixteen characters is a floor the proxy enforces, and it is not fussiness. The wrong-password counter lives in one process's memory, and a host that answers each request from a fresh instance has nothing to share it in — so on the platform this is most likely to run on, rate limiting is best-effort and the length of this value is the defence that survives.
Three configurations, three behaviours:
ADMIN_PASSWORD | What happens |
|---|---|
| Unset | The routes are not claimed at all. The host answers its own 404 and the module says there is no panel here. |
| Shorter than 16 characters | Every route answers 500 AdminMisconfigured, and the module names the reason. |
| Set and long enough | The panel runs. |
The app tells those apart on boot rather than showing a form that can never be right: a 404 means
absent, an AdminMisconfigured code means the password is too short, anything else means locked.
Open /admin and bookmark it
The panel is routed like any other module and appears in the navigation rail like no other module:
never. railHidden is on its descriptor, and it stays on after you unlock — the rail is the list
of places a product's users go, and an item that appeared once the password was accepted would
still be an item anybody could see the moment they borrowed the machine.
/admin is therefore the whole way in, before unlocking and after. The next segment picks the tab
(/admin/health), ?w= names the workspace on the rail and ?b= the bot whose drawer is open.
Typing the password posts it to POST /chatfuel/admin/session. Both sides are hashed with SHA-256
and compared in constant time — hashed first so the buffers are always the same length, because a
length check in front of a constant-time compare gives away how long the real password is. On
success one cookie is set, Path=/, HttpOnly, SameSite=Strict, and Secure whenever the
request arrived over TLS. It is good for twelve hours. Every /chatfuel/admin/* call after that
carries the cookie and an x-cf-admin header — a form posted from another origin can send a
cookie but cannot set a header.
The HMAC key is derived from the password itself and nothing is stored, which is what lets the
panel work on a host that answers each request from a fresh instance. It is also why changing
ADMIN_PASSWORD signs every open session out: the key that signed them is gone.
In front of all of it sits a flat 250 ms pause on every attempt, right or wrong, so a wrong answer costs the same as a right one from the outside. Behind that, three wrong answers are free — a typo must not cost a minute — and then the wait doubles from a second up to five minutes. It counts against the forwarded address, which the caller can forge, so it is a speed bump and the password length is the real defence.
One thing to know before you use it: the admin routes never call the proxy's admission sequence, never consult the auth gate and never apply the workspace fence. This is the only place in the proxy where a fence is skipped, and it is deliberate — those exist to keep a request away from bots that are not the caller's, and the account-wide view they withhold is the panel's entire purpose.
Verify: unlock it and read the Health tab
npm run devOpen localhost:5173/admin and type the password. You land on Bots: the account's workspaces on a rail with their bot counts against the plan, one workspace's bots beside them, and a drawer over one bot.
Then /admin/health, which is the check that the deployment is wired rather than merely
reachable. It reports the upstream base and which variable the token came from, whether the token
is present and whether Chatfuel still accepts it, the account it belongs to, which fence is in
force and how many bots it holds, the auth and admin modes, whether Supabase is configured and
answering, whether the publish queue routes are mounted, the egress route, and the list of
problems the proxy found at startup.
It reports secrets as present or absent and never prints one — a browser screen that showed the
token would be as sensitive as the environment file, and a bot's apiToken is never even selected
from the API.
There is no session revocation other than changing the password. The cookie is stateless, which
is what makes it work on per-request functions, and a stolen one is good until it expires —
twelve hours — or until ADMIN_PASSWORD changes, which invalidates every live session at once.
What to read next
The admin module covers what each tab does and what it refuses to do. The auth gate and the token boundary are the two fences the panel steps around, and environment is the full variable table.
Turn it into a product other people sign up to
Install the auth module and the app stops being your dashboard: strangers sign up, each gets a Chatfuel bot, and one workspace's plan pays for all of them.
Schedule Instagram posts
Chatfuel publishes immediately and stores no post, so a schedule is your own database waking up on a timer and calling your deployment back. The migration, the shared secret, the address it knocks on, and the one call that turns it on.