The environment the wizard reads and writes
Every variable the wizard itself reads, the .env and .env.example it leaves behind, the lock it writes beside them, what --embed appends instead of overwriting, and the guard that refuses to persist a token.
This page covers the environment the wizard reads and the files it leaves behind; the variables the app itself reads at build and run time are on Environment variables.
What the wizard reads
| Name | Where it is read | What it does |
|---|---|---|
CHATFUEL_TOKEN | The token step, first attempt. | Used instead of the prompt, and still checked against the API. Under --yes, or with no TTY, its absence — or its rejection — ends the run rather than falling through to a prompt nobody can answer. |
SUPABASE_ACCESS_TOKEN | Once, as the run starts. | The Supabase Management API token, when --supabase-token is not given. It is resolved into memory rather than read back later, so nothing the wizard spawns inherits it, and it is registered with the log scrubber so no error body can echo it. The auth step and the deploy step both take it from there. |
SUPABASE_SERVICE_ROLE_KEY | The auth step, manual path only. | Taken from the environment instead of being asked for a second time. Because nothing can check that the key belongs to the project being set up, it is named next to that project: at a prompt you can decline it, and under --yes it is used with a warning. |
ADMIN_PASSWORD | The admin step, when --admin-password is not given. | Used as-is, after clearing the same floor the flag clears — sixteen characters, no whitespace — or the run stops saying so. Only when neither is present does the wizard generate one. |
ALLOWED_ORIGINS | The origins step, when --allowed-origins is not given. | The browser origins that may call the proxy besides the app's own. Read the same way the flag is parsed, so a value that is not a list of origins is refused here rather than at the deployment. |
CHATFUEL_APPS_REPO | The app step, when --apps-repo is not given. | Where --app clones the preset catalog from. A catalog that is not the official one is described and confirmed before it is fetched, because a preset is code. Under --yes this variable is refused outright and --apps-repo is what says a catalog is meant: this one can be set in a shell profile and forgotten, and an unattended run has no prompt in which anyone would notice. |
GITHUB_TOKEN, GH_TOKEN | The app step, on a failed clone. | Retried once for an https:// catalog that needs credentials, injected through git config in the environment rather than on the command line. Ambient git credentials are tried first. |
CHATFUEL_CONTENT_REF | Every run, before anything reads a manifest. | The branch to follow, or a full 40-character commit sha to install exactly — which is answered without a network request at all. Unset, the wizard follows main. |
CHATFUEL_CONTENT_ORIGIN | Every content fetch. | An http or https origin to fetch the content from instead of https://raw.githubusercontent.com, for a fork, a test or a machine with no path to the internet. Setting it also pins the run to the commit the wizard shipped with, because resolving a branch against GitHub would name a commit the mirror does not hold. Credentials in the URL are registered with the scrubber before anything can print it. |
CHATFUEL_WIZARD_CACHE | Every content fetch, and the branch resolution. | Where the fetched content and the resolution cache are kept, instead of $XDG_CACHE_HOME/chatfuel-wizard or ~/.cache/chatfuel-wizard. |
XDG_CACHE_HOME | The same, and the gh download. | The base for both caches when neither is set explicitly. On Windows the gh download uses LOCALAPPDATA. |
PUBLISHING_MEDIA_BUCKET | The auth migrations, with the publishing module. | Which Supabase Storage bucket the composer's uploads go into. Resolved once and written into the app's .env as well as into the migration, so both sides name the same string. |
CHATFUEL_BOT_CAP, CHATFUEL_BOT_TOTAL_CAP | The auth migration, as it is applied. | The per-workspace and whole-deployment ceilings on bot creation, substituted into the SQL. Read once by the wizard and never by the app; afterwards they are two functions in your own database. Anything that is not a plain number is left alone, and the file keeps the default it shipped with. |
INIT_CWD | Every path the wizard resolves. | Where you were standing when you started it. A package-manager run moves process.cwd(), so --dir ./app would otherwise land inside the wrong directory. |
npm_config_user_agent | Preflight. | Whether pnpm is even considered. pnpm is chosen only when pnpm launched the wizard and is really installed; otherwise npm, which ships with Node. |
https_proxy, http_proxy, all_proxy, no_proxy | Every outbound call the wizard makes, in lowercase and uppercase, lowercase winning. | Node's built-in fetch ignores these, so the wizard reads them itself — otherwise every call behind a company proxy times out and the token check reports "could not reach the API", which reads as a bad token. NO_PROXY is honoured and loopback is always exempt. doctor prints the one it resolved, with any credentials in it masked. |
TERM, TERM_PROGRAM, and four Windows terminal variables | The module picker. | Whether this terminal can draw the picker's boxes, or gets the plain form. |
Two more are read only to be deleted. npm_config_package and npm_config_call are exported by
npx --package and npx -c to every child process, and a nested npx — which is how the deploy
script reaches the Vercel CLI and how the handoff starts an agent — reads them as its own
configuration and runs the package spec as a command. They are dropped from the process before
anything is spawned.
The file it writes
.env, at mode 0600, in the app the wizard has written. It opens with the four variables that
belong to the deployment rather than to any module — VITE_APP_NAME, VITE_APP_LOGO,
VITE_CHATFUEL_DASHBOARD_URL and ALLOWED_ORIGINS — followed by the union of the selected
modules' declared variables and then an app preset's, first declaration winning. That order is a
rule and not a style: a preset can add variables of its own but can never redefine
CHATFUEL_TOKEN or anything a module declared.
Each value is resolved in one order: a value a step already settled (the workspace, brand, auth,
admin and origins steps), then the Chatfuel token, then the manifest's default, then empty. A
variable that is declared optional and resolves to nothing is written as a commented # NAME=
line, so the file documents it without setting it. A value carrying a line break is refused by
name before the file is written — the format has no escape, so such a value would not produce a
broken line, it would declare a second variable.
.env.example sits beside it. That one is not generated: it travels with the app template and is
copied whole, which is why it carries a paragraph of reasoning above every name instead of a value.
The auth module also declares SUPABASE_PROJECT_REF, which the wizard fills in whenever it knows
the ref. Nothing reads it at runtime and no deploy pushes it — it is bookkeeping, so you can find
the project the app belongs to without opening the URL.
A set-but-empty variable reads as configured everywhere downstream, which is why the wizard
writes # NAME= rather than NAME= and why the deploy script skips both. Filling one of those
in with an empty value is how a deployment ends up half-gated.
npm start loads a .env sitting next to the app, and real environment variables win over it — so
the file the wizard wrote keeps working after a build, and a platform that sets real variables is
never overridden by it.
The lock it leaves beside them
.chatfuel/lock.json is the second file the wizard writes that is not part of the app. It records
what the app was made of, so update can answer the only question that matters when new content
lands upstream: for each file, has anybody edited it since?
| Field | What it holds |
|---|---|
mode | standalone or embed. |
wizardVersion | The version of the wizard that wrote it. |
repo, commit | Where the content came from, and the exact commit. Present only when the wizard ran from a published package — a run from a repository checkout is pinned to nothing anybody else can resolve, and update says so rather than fetching against it. |
modules | The module ids that were installed. |
skills | One entry per skill directory: the module it came with, where its files came from, whether it went into the app or into your home directory, and managed: false for a directory that was already there and was kept. |
files | One entry per file: where the bytes came from as a path in the content repository, the sha256 of what was written to disk, the sha256 upstream held when the wizard changed the bytes on the way in, the name of the producer for a file the wizard generated rather than copied, and the names of any transforms it applied. |
It records provenance and never values. .env is not one of the paths and no entry carries
content — a file whose name starts with .env, the .example pair aside, is never even digested,
because a digest is not the secret but it is a record of a file nobody meant to describe. Only the
trees the wizard actually wrote are walked, which is what keeps an embed's lock from claiming the
host's own files, and a lock that would name a path outside the app is refused rather than written.
The scaffold writes it as part of the app and again once the skills are in; the handoff adds its
two files afterwards; auth amends it when the token rotation has to add a .gitignore line.
What --embed appends instead of overwriting
An embed lands in a project you already have, which may already have a .env. So the step appends
only the keys that file does not define, under a # Added by chatfuel-wizard comment, and never
edits a line that is already there.
| Case | What happens |
|---|---|
| The key is absent | Appended, and reported as added. |
| The key exists with a different value | Left untouched, and reported: .env already defines NAME with a different value. |
The key exists as a # NAME= placeholder the wizard wrote before | Skipped, so a re-run does not append it twice. |
| The file's mode lets other accounts read it | Tightened to 0600 — it now holds a Chatfuel token — and said out loud, because it is a change to a file you did not ask us to touch. |
That tree, those .env keys, a .gitignore line, the skills, the lock and the agent's handoff
files are the whole footprint. No vite.config, no tsconfig and no CSS of yours is edited — the
wiring is the agent's job, guided by the playbooks in the installed skills. The one exception is the
dependency install, which you confirm and your own package manager performs.
The guard that refuses to persist a token
Before either mode writes anything to .env, the wizard checks that git would leave it out of a
commit. It asks git rather than reading .gitignore, because the text of one file is a guess and
git holds the answer: a rule can live in a parent directory, in .git/info/exclude or in
core.excludesFile, and a .env that is already tracked is not made safe by any line added
anywhere — git keeps following a file it already follows. That case is refused outright, with the
git rm --cached .env that fixes it.
Otherwise, if the file is not covered, the wizard asks. Say no and it writes no environment file at
all — the app is still scaffolded, and both the wizard and the agent's instructions then say which
variables you have to create it with. Under --yes the line is appended without asking, and said,
because there is nobody there to ask and the alternative is an unattended run that produces an app
with no token in it.
Either way the line is written and then git is asked again whether it took. A !.env further down,
or a rule in a parent this file cannot override, would otherwise leave the token exposed behind a
success message.
What the wizard does, step by step
The wizard's step sequence in order — what each step asks, what answers it takes non-interactively, what it writes, and which ones reach your Chatfuel or Supabase account.
Scripts in the generated app
Every npm script the scaffolded app ships with — what it runs, when you reach for it, and what has to be true first.