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.
Without the auth module the app is yours: it opens on your Chatfuel account and anybody who
reaches the URL is you. With it, the app is a product. Strangers sign up, each account gets a
workspace of its own with a Chatfuel bot inside it, and the proxy stops trusting the URL and
starts asking your Supabase project who is calling.
One fact shapes everything else. Every customer's bot is created in one Chatfuel workspace —
the one you name in CHATFUEL_WORKSPACE_ID — with your master token. That workspace's plan is
what pays for all of them, so its bot limit is the ceiling for the whole deployment rather than
per customer, and a full one turns the next sign-up into 409 WorkspaceFull. Two caps in your
own database sit in front of that: 20 bots a workspace and 200 across the deployment, both
refusing with 429 BotLimitReached before Chatfuel is ever asked. The numbers, and how to change
them, are on Accounts.
What the app does not do is charge anybody. There is no checkout in it and no billing: the routes
the module adds are /sign-in, /sign-up, /invite/<token>, /forgot-password,
/reset-password, /no-access and /team, and none of them takes money. You pay Chatfuel for
the workspace; how you charge your own customers, and whether you charge them at all, happens
somewhere else entirely.
What an account gets
| Thing | What it is |
|---|---|
| A workspace | One row in cf_tenants, created at sign-up, with a random id the browser never names. created_by is unique, so two tabs cannot open two of them. |
| Bots | As many as the caps allow, each a real Chatfuel bot in your workspace. Adding one is two steps: the database reserves the row and decides whether they may, then the server creates the bot and attaches it. If either half fails the other is undone. |
| Roles | owner (exactly one, cannot be demoted or removed), admin (manages members, invites and bots), member (uses the bots they were granted). |
| Invites | A link, shown once at creation, carrying the role and optionally the bots it grants. The colleague who accepts joins the inviter's workspace and gets no workspace of their own — and no bot. |
| Per-bot access | Granted per person on the Team page. Owners and admins carry no grant rows at all: they reach every bot in the workspace by role, and listing them would read as something you could revoke. |
Take the module
auth is opt-in, which means it is in the interactive picker but --yes never takes it on
its own — it needs a Supabase project, and that is not something to arrange behind somebody's
back. Name it:
npx @chatfuel/wizard --modules auth,livechat,contactsIt is also a hidden module: no rail item, no /auth address. It wraps the whole shell in its gate
instead, puts the user menu in the top bar and answers /team. The one npm package it brings is
@supabase/supabase-js.
Adding it to an app that already exists is the --embed story rather than this one — see
Add a module.
Point it at a Supabase project
The users live on your Supabase project, not ours. The free plan is enough, and the wizard offers two ways to reach it.
Generate a personal access token at supabase.com/dashboard/account/tokens and paste it when asked. The wizard then picks or creates the project, waits for it to come up healthy, reads the API keys, applies the migrations and configures email + password sign-in. The token is used once, kept in memory, and written nowhere.
A fine-grained token needs the scopes the wizard names when it asks; a classic all-access token works too.
In a scripted run, --supabase-create <name> reuses a project already carrying that name instead
of making a second one, so re-running the script does not spend the account's other free project
on a duplicate. And a project that already carries a Chatfuel install is named out loud before
anything is applied: re-running for the same deployment is the update path, while a second
deployment on one project shares its accounts, its bots and its publish queue.
Set the variables
Two of them decide whether the gate exists at all:
VITE_SUPABASE_URL=https://<ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<publishable key>Both set turns the gate on. Neither is open mode. One without the other is fail-closed — every
proxied request answers 500 ProxyAuthMisconfigured, because the proxy refuses to guess which
half you meant.
Two more do not touch the gate but decide whether it is any use:
| Name | Why |
|---|---|
SUPABASE_SERVICE_ROLE_KEY | Server-side only. Without it the provisioning route is not mounted, so the server cannot register a bot it just created and sign-up ends on "your workspace is not ready". |
CHATFUEL_WORKSPACE_ID | The Chatfuel workspace every customer's bot is created in. Without it the route refuses rather than creating bots that bill nowhere. |
CHATFUEL_WORKSPACE_ID is unrelated to VITE_CHATFUEL_WORKSPACE_ID, which stops meaning anything
once this module is on: the workspace picker disappears and the bots come from the session.
Environment variables has the full table.
The wizard tells you how much room the workspace has when it asks which one to use, and warns you outright when it holds a single bot — then only the first account through the door gets one.
Run the migrations
Your app has a supabase/ directory, and it is yours. On the access-token path the wizard has
already run these against the project; on the manual path nobody has.
supabase/migrations/
0001_chatfuel_auth.sql the whole schema: cf_profiles, cf_tenants, cf_members,
cf_bots, cf_bot_members, cf_invites, cf_migrations,
cf_invite_probes, cf_recovery_events, cf_resource_owner,
cf_admin_attempts and every cf_* RPC
0010_chatfuel_publishing.sql only with the publishing module: the publish queue
0020_chatfuel_admin.sql only with the admin module: its service-role functionsApply them in name order — everything above 0001 reads what 0001 makes — by pasting each
into the SQL editor at https://supabase.com/dashboard/project/<ref>/sql. Every file is
create … if not exists / create or replace throughout, so a second run changes nothing. The
wizard's own run stops at the first failure rather than attempting the rest, for the same reason.
There is nothing to seed. A workspace appears when somebody signs up.
Later schema changes go in a new file, 0002_….sql — editing one that already ran makes two
deployments drift. The numbering leaves each module a block of ten, which is why the publishing
module's file arrives as 0010_ and the admin module's as 0020_.
The two bot caps live in 0001_chatfuel_auth.sql as one-line functions. Setting
CHATFUEL_BOT_CAP or CHATFUEL_BOT_TOTAL_CAP in the environment of this wizard run writes your
own numbers into the file as it is applied; afterwards they are SQL in your database, and
re-running the create or replace is what changes them.
Finish the Supabase settings
On the access-token path the wizard configures sign-in for you, on a project it created, and asks who may open an account:
--signup | What it asserts |
|---|---|
open | Sign-ups on, addresses taken on trust. The default: a fresh project has no SMTP, so a confirmation mail would never arrive and nobody could sign in. |
confirm-email | Sign-ups on, and a new account must confirm its address. Needs mail that arrives — the built-in sender manages about two an hour. |
closed | The sign-up form is off; you create accounts in the Supabase dashboard. |
On a project you brought, the wizard leaves all three alone and says so — a closed sign-up stays closed and a turned-on confirmation stays on. Then, by hand or to check:
- Authentication → Providers → Email. "Confirm email" off is what
openmeans; with it on, the default provider rejects addresses it considers undeliverable and caps the rest at two emails an hour. - Authentication → URL configuration. The redirect allowlist has to hold
http://localhost:5173/**and your deployed origin,https://app.example.com/**. The wizard merges the app origin either way, and adds the localhost entry only to a project it made. SUPABASE_SERVICE_ROLE_KEYin the app's.env(Project Settings → API Keys → secret). Thecf_bot_createdfunction is granted toservice_roleand to nobody else.
Password-reset mail needs your own SMTP (Authentication → SMTP). Until you configure it,
admins issue reset links from the row menu on the Team page — those open in any browser, while the
emailed link on a free project carries a PKCE code that only works in the browser that asked. That
route delivers by writing the link to the server log, so it is off unless you turn it on:
AUTH_RECOVERY_LINK_LOG=true, plus PUBLIC_URL for the address the link points at. The wizard
asks about it once, and the reasoning is on Accounts.
Sign up as your own first customer
Start the app and open /sign-up. Do this before you share the URL.
npm run devThree things prove it. The account exists: sign-up returns a session immediately, with no
email round-trip. The bot exists: the app calls POST /chatfuel/auth/provision, the server
creates a real Chatfuel bot in CHATFUEL_WORKSPACE_ID with your master token, and the bot appears
in the top-bar switcher — go and look at it in the Chatfuel dashboard, it is really there. The
gate is on: read the startup line, which begins chatfuel proxy: auth gate on (bots per account in workspace and then names the workspace and every route it mounted.
The startup log is the only place the mode is named. The health route is the same path on every host — the dev server, the bundled Node server and the Vercel function — and answers liveness and nothing else, because it is open to anyone who can reach the deployment:
GET /chatfuel/healthz → {"ok":true}It answers 503 with {"ok":false} when the proxy found a configuration problem at startup, and
a missing CHATFUEL_WORKSPACE_ID is one of them. Which problem it is, is in the log beside the
mode.
Then open /team, invite a second address, and check that the invited account lands in your
workspace rather than getting one of its own.
Under open sign-up, anybody who can reach the URL can create an account, addresses are never
verified, and every account spends a bot from your Chatfuel plan — as does every extra bot they
add afterwards. A workspace whose plan holds one bot gives it to the first person through the
door, and that need not be you. The server says so at every boot while sign-ups are open. To
close the door, use --signup closed or the project's own Allow new users to sign up
toggle; to restrict which accounts get a workspace, put a check inside cf_claim_workspace in
a new 0002_….sql.
The gate itself — what it checks, what it caches for thirty seconds, and the operations no fence can cover — is the auth gate. The codes it answers with are in Errors, and the module's own surface is Accounts.
Make it yours
The app's name and mark live in two environment variables and one file in public/ — except the browser tab, which is edited in index.html on disk because the head is parsed before any environment is read.
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.