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

Page: https://sdk.chatfuel.com/docs/guides/multi-tenant-saas
Markdown: https://sdk.chatfuel.com/docs/guides/multi-tenant-saas.md

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

<Steps>
  <Step>
    ### Take the module [#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:

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

    It 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](/docs/guides/add-a-module).
  </Step>

  <Step>
    ### Point it at a Supabase project [#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.

    <Tabs items="['Access token', 'Manual']">
      <Tab value="Access token">
        Generate a personal access token at
        [supabase.com/dashboard/account/tokens](https://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.
      </Tab>

      <Tab value="Manual">
        Paste the project URL and the anon key. The wizard makes no Supabase API call at all — it writes
        the values into `.env`, copies the SQL into `supabase/migrations/`, and prints what to run and
        what to switch on. Steps four and five are then yours to do by hand.
      </Tab>
    </Tabs>

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

  <Step>
    ### Set the variables [#set-the-variables]

    Two of them decide whether the gate exists at all:

    ```bash
    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](/docs/deploy/environments) has the full table.
  </Step>

  <Step>
    ### Run the migrations [#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 across
    ```

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

  <Step>
    ### Finish the three Supabase settings [#finish-the-three-supabase-settings]

    The access-token path sets the first two for you.

    1. **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.
    2. **Authentication → URL configuration.** The redirect allowlist has to hold
       `http://localhost:5173/**` and your deployed origin, `https://app.example.com/**`.
    3. **`SUPABASE_SERVICE_ROLE_KEY` in the app's `.env`** (Project Settings → API Keys → secret). The
       `cf_bot_created` function is granted to `service_role` and 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.
  </Step>

  <Step>
    ### Sign up as your own first customer [#sign-up-as-your-own-first-customer]

    Start the app and open `/sign-up`. Do this before you share the URL.

    ```bash
    npm run dev
    ```

    Three 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:

    ```json
    { "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.
  </Step>
</Steps>

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

The gate itself — what it checks, what it caches for thirty seconds, and the operations no fence
can cover — is [the auth gate](/docs/concepts/auth-gate). The codes it answers with are in
[Errors](/docs/reference/errors), and the module's own surface is
[Accounts](/docs/modules/accounts).
