Modules
A module is a directory, a manifest and a React tree — plus two registration tables, one generated by the wizard and one curated by hand.
There is no plugin registry to publish to, and nothing in the running app loads a module. A module is a directory, a manifest and a React tree; the manifest is read while the wizard runs and has no presence afterwards. By the time the app starts, "which modules are installed" is not a question — it is the set of directories that exist.
What a module is made of
Two halves, in two places. The content half describes the module and teaches an agent about it; the app half is the React tree the shell mounts.
index.tsx exports one thing under a fixed name, moduleDescriptor — the id, the title, the icon
and a lazily imported root component. Everything else in the subtree is the module's own business:
module code imports React, ~ui, ~api, the shell contract and its own files, and nothing else.
The manifest
module.json is validated against a JSON Schema, so a field that does not exist is a failed gate
rather than a silent no-op, and the TypeScript type beside the schema has to move with it.
| Field | What it decides |
|---|---|
id | the directory name on both sides, and the module's first path segment |
status | ready is offered by the picker; planned never is |
selection | opt-in is offered but never taken by --yes |
hidden | contributes no rail item and no route of its own |
requires | module ids that must be installed alongside it |
recommends | ids the wizard suggests, and you can decline |
skill.installAs | the directory name the agent skill is installed as |
app.env | the environment variables this module needs, and which wizard step supplies each |
app.embed | the roots and entry component that --embed copies into a host project |
permissions | the Chatfuel permissions its surfaces need |
status: "planned" is how a module lands in the repository before it is ready to be chosen. It is
in the tree, it is validated like the others, and the picker never offers it.
Scaffolding is subtractive
The wizard copies apps/shell whole and then deletes. Module subtrees you did not pick are removed,
the registry is regenerated from what is left, the navigation table is filtered to the same set, and
the tsconfig paths that no longer resolve are pruned.
What you get is not a template with holes in it. There is no dead import, no commented-out route and no path pointing at nothing — the code you did not ask for was never written into your project at all.
Two tables register a module, and only one is yours to edit
They are different on purpose.
src/modules/index.ts is generated. The wizard rewrites it from the modules you picked, in
directory order. Editing it by hand is fine until the next scaffold.
src/modules/navGroups.tsx is hand-curated — the menu's information architecture, and the one
place it lives. It is a table of groups (AI Agent, Live Chat, CRM, Growth) naming module ids in the
order they should read. The wizard filters that table to the modules this app installed rather than
rewriting it, so the menu reads the same way in every deployment.
Three behaviours follow from keeping them apart:
- an id in the table that was never installed vanishes, and a group left empty does not render;
- a module that nobody placed in a group still appears, under a fallback heading — a menu that silently dropped a page would be worse than one with an extra heading in it;
- when only one module is left there is no nav at all.
Two flags take a module out of the menu, and they are not the same thing. hidden removes the
route as well — auth is hidden because it wraps the shell instead of being a page in it.
railHidden keeps the route and only stops the listing: the admin panel is reached by typing its
address, which is the point of it.
requires and recommends
A module may not import another module. requires is the exception, and it is a knowledge
dependency rather than an import path — deals requires contacts because it is a view of the same
records. recommends is a suggestion the picker makes and you can decline: livechat recommends
contacts, admin recommends auth.
For the cases in between, the shell tells each module which ids this deployment actually has. That is
how knowledge-base can link into bookings when bookings is installed and edit the same data
itself when it is not — without importing it, and without guessing.
Permissions are declared, not discovered
Each manifest also names the Chatfuel permissions its surfaces need — People / View to read the
deals board, People / Edit to move a card between stages. A token whose account lacks one gets a
refusal from the API rather than a blank screen, and the module knows which permission the call
wanted and says so.
The wizard reads the same list when it writes the handoff notes, so the agent that continues the work knows what the token has to be allowed to do.
What each module actually does, screen by screen, is in the module reference.
- AI AgentAI replies on every channel
- Flow BuilderVisual editor for chat flows
- Knowledge BaseEverything the AI knows
- CopilotAI copilot inside your app
- InboxShared inbox for your team
- ContactsCRM for your audience
- DealsKanban board for sales
- BookingsCalendar for appointments
- PublishingInstagram posts and Reels
- Ads OptimizationAd conversions back to Meta
- AccountsSign-up for your customers
- AdminOperator panel for the account
Vendoring, not depending
The design system, the API client and the proxy arrive as source you own — the trade that makes, and the two rules that keep the copies working.
Routing
The shell owns the address bar and hands each module a view and its params, which is why a module never touches window.location.