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 WorkspaceFull. Nothing in the app caps
how many bots an account creates; the Chatfuel plan is the cap.
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, 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 they add, 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. |
An earlier schema gave a workspace exactly one bot. That stopped being true with the
0002_multi_bot migration, and any wording that still says "one tenant per deployed bot" is
stale.
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 switches email + password sign-in on. 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.
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.
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
and every cf_* RPC
0002_chatfuel_multi_bot.sql moves a project that ran an older 0001 acrossApply them in name order — 0002 moves 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 and the data migrations are guarded, so a second run changes
nothing. On a project the current 0001 created, 0002 does nothing at all.
There is nothing to seed. A workspace appears when somebody signs up.
Later schema changes go in a new file, 0003_….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_.
Finish the three Supabase settings
The access-token path sets the first two for you.
- Authentication → Providers → Email → turn OFF "Confirm email". A fresh project has no SMTP, so a confirmation mail never arrives and nobody can sign in. It is also what makes sign-up work on the free plan at all: with confirmation on, the default provider rejects addresses it considers undeliverable outright and caps the rest at two emails an hour, after which every sign-up is rate-limited.
- Authentication → URL configuration. The redirect allowlist has to hold
http://localhost:5173/**and your deployed origin,https://app.example.com/**. 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.
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: the dev server has no health route, so it says so on its startup line, which begins
chatfuel proxy: auth gate on (bots per account in workspace and then names the workspace and
every route it mounted.
On a deployment, ask the health route instead — /healthz on your own server,
/chatfuel/healthz on Vercel:
{ "ok": true, "auth": "on" }off means the two Supabase variables never reached the server. misconfigured means one of them
did and the other did not. On Vercel the same answer also carries a problems array, and the
status is 503 rather than 200 when it is not empty — a missing CHATFUEL_WORKSPACE_ID shows up
there as ProxyWorkspaceMissing.
Then open /team, invite a second address, and check that the invited account lands in your
workspace rather than getting one of its own.
Sign-up is open to anybody who can reach the URL, addresses are never verified, and every
account that signs up 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. To restrict who may sign up, put a check inside cf_claim_workspace
in a new 0003_….sql; to cap bots per account, put one in cf_new_bot.
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.