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
so is an ADMIN_PASSWORD in the environment that would not clear the floor, rather than leaving
the failure for the deployment to find at boot. 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.
Install auth alongside it wherever the panel runs on more than one instance. The
wrong-password counter lives in one process's memory, and with the accounts module the count is
kept in your database as well (cf_admin_attempts), so every instance shares one and a run of
guesses spread across them is counted as one.
Installing it is not enough on its own, though: that shared counter is written with
SUPABASE_SERVICE_ROLE_KEY, which the proxy treats as optional. With the module on and the key
unset there is still nowhere to keep a shared count, and a stateless host is back to a
best-effort counter in one process's memory. On such a host, set the key. If you cannot, keep the
panel off it — leave ADMIN_PASSWORD unset there and run the panel somewhere you control, or put
your own authentication in front of it.
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. Without a shared counter the wrong-password count lives in one process's memory, and a host that answers each request from a fresh instance gives every guess a fresh count — so the length of this value is the defence that survives. Use what a password manager generates.
And protecting /admin is yours to do: nothing in the template does it for you. This
password is the only thing in front of the whole Chatfuel account behind CHATFUEL_TOKEN —
there is no second factor, no IP allowlist, no lockout that outlives a process, and no owner to
appeal to if it is guessed. Setting it on a host the internet can reach is a decision, and it is
yours to make.
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 everywhere but
plain HTTP to loopback. It is good for two 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 — and the panel answers its own origin and no other.
The cookie is signed with a key derived from the password by scrypt, salted per deployment. The payload is entirely predictable, so anyone who has seen one cookie holds a known plaintext and its MAC and can guess the password offline with nothing to throttle; scrypt is what makes each guess expensive, and a per-deployment salt is what stops one precomputed table from answering every deployment running this code. Nothing is stored, which is what lets the panel work on a host that answers each request from a fresh instance.
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. Where the
request arrived through an edge this deployment does not trust, everybody shares one bucket and
the ceiling drops to thirty seconds instead, because a long lockout is then something one caller
imposes on the admin. Every admin route also caps the body it will read: 64 KiB, and 4 KiB on
/session, which is the one route reached before any credential is checked.
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. Six tiles — the Chatfuel token and whether it is still accepted, the account it belongs
to, which fence is in force and how many bots it holds, the sign-in mode, whether the database
answers a ping, and whether the publish queue's routes are mounted — over four plain lines: the
Chatfuel API, the token variable's name, the home workspace and the egress route. Anything the
proxy found wrong at startup is listed above them by code. Underneath sits the button that turns
scheduled posts on.
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.
Rotating ADMIN_PASSWORD is the revocation that holds everywhere. Signing out revokes the
sessions issued by the instance that served it, through a watermark in that process's memory —
on a host that answers each request from a fresh instance, no other instance has heard of it, so
a stolen cookie is good there until it expires. Rotate the moment a cookie or the password may
have gone astray, rather than signing out and rather than waiting. If the password itself is
fine and only the sessions are not, ADMIN_COOKIE_SALT is the second lever: setting it
invalidates every open admin session without changing the password, and it is also what to set
when two deployments share a password and must not share cookies.
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 click that turns it on.