Skip to content
ChatfuelSDK
Modules

Accounts

Sign-in for your own customers on your own Supabase project — one workspace an account, a bot each, and the proxy turned into a per-bot gate.

What it is

This is the module that turns the app into a product other people sign up to. Every account that signs up gets a Chatfuel bot of its own, and can create more — all of them made by this app's server with your master token, inside one workspace of their own that they never learn is one of many. Colleagues arrive by invite and join the inviter's workspace instead of getting one of their own.

The manifest still calls it "one tenant per deployed bot". That has been untrue since migration 0002_multi_bot.sql moved bots out of cf_tenants.bot_id and into cf_bots: the model is one tenant, many bots.

Sign-in is Supabase Auth — email and password — on your Supabase project, whose data and keys are yours. The Chatfuel token never leaves the server; what reaches it is the proxy, which has become a gate.

Installed as

Wizard idauth
Install itnpx @chatfuel/wizard --modules auth
Selected byOpt-in--yes never installs it
RequiresNothing, beyond the implicit core
Recommends
Skillchatfuel-auth

Opt-in because it needs credentials the wizard cannot invent: a Supabase project. --yes installs every other ready module and skips this one with a line saying so; name it explicitly to get it.

It is also hidden, which in the manifest means exactly one thing: no rail item and no /auth route. It contributes a host integration instead — the shell wraps itself in the module's Gate, puts the module's item in the user menu, and hands it the routes below. What it changes is who may reach everything else.

Routes and views

Eight routes, and none of them is /auth. The shell knows these first path segments are the module's and never treats them as module ids.

PathRendered
/sign-inInstead of the shell
/sign-upInstead of the shell
/invite/<token>Instead of the shell
/forgot-passwordInstead of the shell
/reset-passwordInstead of the shell
/authInstead of the shell — the callback a mailed link lands on
/no-accessInstead of the shell
/teamInside the shell chrome

team is the only one in IN_SHELL_ROUTES, and it is opened from the avatar menu rather than from the rail — and only for an owner or an admin. It is one page: members and invites in a single table with a Bots column that opens each person's access, a Bots table (new, rename, delete), Invite people, and a danger zone holding leave and transfer ownership.

Everything here is written app-relative, because only the shell knows where the app is mounted. The two links that have to be absolute are the two that get mailed — an invite and a recovery link — and the runtime hands those the mount point. A returnTo is app-relative too, is rejected if it names another origin, and never bounces back into an auth screen.

The model

One workspace per account is a database constraint, not a convention: cf_tenants.created_by is unique, which is also what makes sign-up safe from two tabs. At most one owner is a partial unique index on (tenant_id) where role = 'owner'.

RoleWhat it is
OwnerOne per workspace. Cannot be removed, demoted, or leave without transferring first.
AdminManages members, invites and bots, and reaches every bot without a grant. Cannot touch the owner row.
MemberUses the bots they were granted, on the Team page or by the invite that brought them in.

Reaching every bot and administering the team are the same pair of roles today and are kept as two separate rules, in the SQL and in lib/roles.ts, because they are different questions. Owners and admins hold no grant rows at all — their access comes from the role, and listing it would read as something revocable.

Every customer's bot comes out of one Chatfuel workspace. CHATFUEL_WORKSPACE_ID names it, the wizard asked which one, and its plan pays for all of them. So its botsLimit is the ceiling for the whole deployment rather than per customer, and when the allowance runs out both sign-ups and New bot answer 409 WorkspaceFull. Nothing in the app caps it; a per-account cap belongs in a check inside cf_new_bot. The alternative — plain createBot — is not a fallback: it puts each bot in a throwaway workspace with a limit of one and nobody's plan behind it, and that workspace outlives the bot. A deployment without CHATFUEL_WORKSPACE_ID refuses to provision at all.

Two trust boundaries do the work, and they answer different questions:

QuestionMechanism
PostgREST + the cf_* RPCsWhat may this user do inside the workspace?security definer functions; the tables have RLS on with no policies, so the RPCs are the entire read/write surface. A missing capability is a missing RPC, never a new table policy.
The proxy gateMay this request reach Chatfuel at all, for this bot?The proxy calls cf_my_bot_ids with the caller's JWT and fences every request against the answer.

The second one is the isolation boundary, because behind it there is a single Chatfuel account holding every customer's bot. The fence is exact for anything that names a bot — botID in the variables, the root bot(id:) field, every WebSocket subscribe frame — and answers 403 BotNotAllowed without the request ever reaching Chatfuel. Reads that reach for the account (currentUser beyond id and botRole, and botsV2) are refused with 403 AccountScopeBlocked, precisely because they would hand one customer the list of all the others. An empty set is still ok: true: signed in, no workspace yet — the state the provisioning route exists to leave.

The gate caches its answer for 30 seconds per sha256(jwt), and it runs on a WebSocket at connect time rather than per frame. So a member you just removed keeps working over HTTP for up to half a minute, and keeps an already-open subscription until that socket drops. Nothing errors in the meantime. Closing sockets on a role change is named as a follow-up, not implemented.

Configuration

VariableNotes
VITE_SUPABASE_URLYour project's URL. Both sides.
VITE_SUPABASE_ANON_KEYThe publishable key. Both sides.
SUPABASE_SERVICE_ROLE_KEYSecret, and not optional under auth: it registers each bot the server creates and mounts the password-recovery route.
SUPABASE_PROJECT_REFOptional.
CHATFUEL_WORKSPACE_IDThe one Chatfuel workspace every customer's bot is created in. Unrelated to VITE_CHATFUEL_WORKSPACE_ID.

The wizard fills all but the workspace pick from a Supabase personal access token, which it uses once, keeps in memory, and writes nowhere. What each variable is and which side reads it is on environment variables.

Permissions

The manifest declares no Chatfuel permissions at all. This module asks nothing of the Chatfuel token — it decides who may use the one the deployment already holds.

What stands in their place is two lists. Inside a workspace, the cf_* RPCs are the whole surface: cf_profiles is the one table with a direct grant (select and update, self only), and tenants, members, bots, grants and invites are reachable by function or not at all. On the way out, the gate's answer to cf_my_bot_ids decides every request.

Limits

Saved views and preferences are lost under this module. currentUser.userStorageItem is storage against the Chatfuel account, so allowing it would hand every customer the same list — the proxy answers AccountScopeBlocked. Deals' saved views and Bookings' preferences both degrade to "nothing saved yet". A per-account version would live in Supabase, not in Chatfuel.

Removal is a real lock-out. Somebody removed from a workspace loses it; signing up again gives them a new, empty bot rather than the one they were removed from. That is the difference the bot boundary makes, and it is worth saying out loud before an admin clicks Remove.

Anyone who can open the app can create an account, and each one creates a bot. There is no billing, no cap, and no switch for it — that is what a self-serve product is, and it is also the cost model. A restriction is a check in cf_claim_workspace in a migration of your own.

Sign-in is email and password only. OAuth and magic link are not wired; the adapter and a footer slot on the sign-in screen are where they would go. There is no second factor and no admin approval step. And Forgot password cannot deliver mail until you configure SMTP on the project — until then an admin issues a reset link from the Team row menu.

The workspace has no settings. It is named after the account that created it and is not editable in the app: one less write path.

Workspaces per account1 (created_by is unique)
Owners per workspace1 (a partial unique index)
Bots per accountUncapped here — the Chatfuel workspace's plan is the ceiling
Invite token24 random bytes; only the hash is stored, and the raw token is shown once
Invite expiry7 days by default; over 0 and at most 30; the dialog offers 1, 7 or 30
Password floor8 characters (Supabase's own)
Profile name60 characters
Bot name80 characters
An unfinished bot reservationSwept after 10 minutes
Gate cache30 seconds, bounded by the JWT's own exp

On this page