# 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.

Page: https://sdk.chatfuel.com/docs/guides/admin-panel
Markdown: https://sdk.chatfuel.com/docs/guides/admin-panel.md

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.

<Steps>
  <Step>
    ### Install the module [#install-the-module]

    `admin` is opt-in, which means `--yes` leaves it out. Name it:

    ```bash
    npx @chatfuel/wizard --modules admin,livechat,contacts
    ```

    The 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.

    ```bash
    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.
  </Step>

  <Step>
    ### Put the password in the server environment [#put-the-password-in-the-server-environment]

    <Tabs items="['Locally', 'On Vercel']">
      <Tab value="Locally">
        ```bash title=".env"
        ADMIN_PASSWORD=<16 characters at least>
        ```

        Unprefixed, beside `CHATFUEL_TOKEN`. A `VITE_` name would put it in the browser bundle.
      </Tab>

      <Tab value="On Vercel">
        `npm run deploy` pushes it with the rest of `.env`, as a Vercel **sensitive** variable — so it
        cannot be read back out of the dashboard — to production and preview, with `--force` so
        re-running overwrites rather than adding a second value.
      </Tab>
    </Tabs>

    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.
  </Step>

  <Step>
    ### Open `/admin` and bookmark it [#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.
  </Step>

  <Step>
    ### Verify: unlock it and read the Health tab [#verify-unlock-it-and-read-the-health-tab]

    ```bash
    npm run dev
    ```

    Open [localhost:5173/admin](http://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.
  </Step>
</Steps>

<Callout type="warn">
  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.
</Callout>

## What to read next [#what-to-read-next]

The [admin module](/docs/modules/admin) covers what each tab does and what it refuses to do.
[The auth gate](/docs/concepts/auth-gate) and [the token boundary](/docs/concepts/token-boundary)
are the two fences the panel steps around, and [environment](/docs/reference/environment) is the
full variable table.
