Installing and packaging modules
Installing and packaging modules
Who this is for: integrators and implementers who set up projects for clients: by script, through an AI agent, or by exporting a configured folder and importing it elsewhere.
What it covers: the module catalogue and its ids, installing modules through MCP tools and REST, the module package format, the API actions that move stock and money between modules, and the operations you use afterwards. The owner-facing walk-through of the same feature is . The building blocks (tables, columns, forms, dashboards, portal) are in .
1. Concepts in one paragraph
A module is a capability such as Invoicing & Payments, Bookings or Inventory & Stock. Installing a module creates a set of data tables, each with a purpose (its meaning, such as core_invoice or core_stock_movement), then wires them together: reference columns point at the right tables, rollups and lookups are bound, workflows, views, dashboards and automations are added. A business type (also called a vertical) is only a curated list of module ids. "Salon" is bookings plus invoicing plus a customer book. Nothing the installer creates is locked: after install every table is an ordinary table you can reconfigure. See for purposes and behaviours.
Modules compose by purpose, not by name. A module provides the purposes its tables carry and requires purposes it needs from others. The installer resolves each requirement to the first module that provides it, adds that module, and installs in dependency order.
Careful: installing needs the
Ownerright on the project. Through MCP it needs theadminscope of the token, which only a project owner can hold.
2. The module catalogue
There are 33 built-in modules in three tiers. The full list, with the tables each module creates, what it needs, its status flows and every business type, is in . How the installer works inside is in .
Foundation (9 modules): shared single sources of truth (people, contacts, customers, products, services, categories, price lists, locations, ledger). A foundation table that already exists in the project is bound instead of duplicated.
Domain (24 modules, shown as Processes in the picker): the processes a business runs, such as billing_ar, bookings, inventory_stock or field_service. A domain table is always created fresh, so a second install gives a second set.
Extension: a tier for overlays. The built-in catalogue has none, but imported packages may use it (shown as Extensions).
Two modules, webshop_site and booking_site, create a public site item rather than only tables (the picker marks them Public site). Going live is a separate step: open the site and use Publish to web.
Self-hosted only. Needs your administrator: a site goes live only if your administrator has registered a storefront template of the matching type (System, Config, Deployments).
Call list_process_modules to read the catalogue live, with each module's provides, requires, enhances, tableCount and the project's install state (section 3).
3. Installing programmatically
Two equivalent paths feed the same installer: MCP tools (for agents) and a REST route (for everything else). The installer expands hard dependencies, binds foundations to existing tables and runs each module's wiring.
MCP tools
Tool | Scope | What it does |
|---|---|---|
| user | Lists the catalogue, optionally filtered by |
| user | Dry run, no writes. Input |
| admin | Input |
Careful:
preview_module_installplans against an empty project. Itsactionis therefore alwayscreate, even for a foundation table the project already has. The real install binds that table instead. To see what the project already holds, readinstalled,installedPurposesandmissingPurposesfromlist_process_modules.
Careful: the MCP tools know only the built-in catalogue. A module package you imported (section 4) is installable from the Set up from modules dialog or the REST route, not from
install_modules.
REST
POST /api/projects/{projectId}/modules/apply. Needs the Owner right. Body:
Field | Type | Meaning |
|---|---|---|
| string[] | Required, non-empty. Built-in module ids and the ids of packages imported into this project |
| number or null | Folder to install into. Default is the project root |
| string | Creates one folder with this name to hold the new tables. Bound (existing) tables stay where they are |
| boolean | With |
| string | A business type id (see below). Ignored if the business type has no sample data |
Response (200): { ok, folderId, created, bound, tablesByPurpose, foldersByKey, unresolved, problems, warnings }. created and bound map a table template id to a document id. tablesByPurpose maps every involved purpose to its document id. unresolved lists reference columns left unbound. Errors: 400 no_modules for an empty list; 400 install_failed with problems when nothing could be installed (for example all ids unknown); 403 without the Owner right; 500 install_failed for a database failure.
Sample rows are written best-effort and only into tables created by this call. A failed sample row becomes a warning and never fails the install.
Facts that hold on both paths
A business type is just a module-id set. There is no separate "install a business type" call. Resolve the business type to its module ids (core plus the add-ons you want) and pass that array. The 18 business types, with their ids, core modules and suggested add-ons, are in . The business type id is also the verticalId value for sample data. Seven business types accept it: salon_spa, fitness_studio, health_clinic, auto_repair, retail_store, restaurant and agency_consulting.
Hard dependencies are pulled for you. List only the modules you care about. Anything they require is added. Capabilities that a module merely enhances are not pulled, so list them yourself if you want them. This is why business types name foundation_customers explicitly.
Installing again is additive. Foundation tables bind to what already exists. Domain tables are created fresh.
4. Module packages: build once, ship to many clients
A module package is a portable, structure-only export of a configured folder. Build the setup in a scratch project, tune it, export it, and import it into client projects.
Authoring loop: scratch project, configure, Export as module…, hand over the JSON, Import module… in the client project, install from Set up from modules.
Action | Where | Notes |
|---|---|---|
Export a folder | Folder menu Export as module…, or | Body |
List packages of a project |
| Returns |
Import a package | Import module… in Set up from modules, or | Body is the package JSON. Returns |
Remove an imported package | The trash icon on the module in the picker, or | Removes the package only. Tables already installed from it stay as ordinary tables |
All four need the Owner right.
Where a package comes from
Imported (community): stored in one project. It is removable and marked Community in the picker.
Official: first-party packages shipped with the product and available in every project, marked Official and not removable. This release ships none, so official in the list response is empty. If a project imports a package with the same moduleId as an official one, the import wins.
What travels, and what does not
Travels | Does not travel |
|---|---|
Table structure: columns, types, and links between tables by meaning (the target table's purpose, not its id) | Row data. A package is structure only |
Status workflows (states and transitions) and required-column lists | Portal exposure. It is forced off on import; share again after install |
Automations, saved views and dashboards, replayed on install | Forms, documents, spreadsheets, sites and connectors. Only tables, their automations and views, and dashboards are collected |
Folder layout, so tables and dashboards land in their original sub-folders | Anything on a table that has no purpose (see below) |
Careful: set a purpose on every table in the folder before you export. Views and automations on a table with no purpose are skipped, a reference column that points at a table with no purpose will not link on install, and dashboard panels bound to spreadsheets are dropped. Each of these is listed in
warnings.
Validation and format
Import is the trust gate for packages from other people. It refuses a package with a wrong version, an unmet requires, a column pattern that could hang the server, or an automation URL that is not https and public. It also forces portal exposure off. The package file layout (module, dataTableTemplates, wiringSteps, layout), the exact checks and the placeholders ($purpose:, $folder:, $bindings) are in . Bump module.version on every change you ship.
5. Cross-module actions
These are the points where one module's action writes into another module's data. Call them instead of hand-writing ledger rows. Each one creates its movement rows in a compensated sequence (if one write fails, the rows already created are removed) and then moves the source row to its final state.
Route | Body | What it does |
|---|---|---|
|
| Books every purchase-order line's ordered quantity into stock (plus rows in the movements ledger), then sets the order to |
|
| Issues every sale line's quantity out of stock (minus rows), then sets the sale to |
|
| In one operation, consumes each component of the bill of materials (minus rows) and produces the output (a plus row), scaled by |
|
| Draws |
Rules that apply to all four:
Authentication is the signed-in user's session cookie, as in the app itself. No MCP tool and no bearer-token entry point exists for these actions, and the built-in screens have no button for them.
Rights: Edit on the source table (purchase orders, sales, production orders or invoices) and Edit on the movements ledger (or, for store credit, on the wallet and payment tables). Store credit also needs Read on the customer table.
Everything is derived. The body only names the source row. The lines, the ledger (found through the catalogue's on_hand balance column), the location and the customer come from the data.
No stock module, no stock write. If Inventory & Stock is not installed, the call succeeds with stockReceived, stockIssued or stockPosted set to false and a reason.
Repeat calls are refused with 409 once the row is already received or completed (when the table has that status), so a double click does not inflate stock.
Negative stock is allowed. Issuing past zero does not fail. Shortages show up in the low-stock view.
Production is single level. A component that is itself manufactured is not expanded.
Refunds do not return stock. Moving a sale to refunded changes its status only.
Common error bodies are { "error": "<code>" }: 400 for a missing or malformed body or a table of the wrong kind, 404 for a missing table or row, 409 for already processed, 422 for a source row that cannot be processed (no lines, no BOM, no quantity).
Because the stock ledger is the shared seam, a plus row from Purchasing and a minus row from Point of Sale land in the same place, which is why the on-hand balance on the catalogue is always their running sum.
6. Operating the data
After install, drive the project with MCP tools. Call datatable_get_schema first to learn the column ids.
Tool | Purpose |
|---|---|
| Column ids, names and types, plus a write guide for shapes that need ids (people, groups, assets) |
| Read rows with pagination, filtering and sorting |
| List saved views (grid, kanban, calendar and so on) |
| Create one row ( |
| Partial update of one row or many: only the columns you send change |
| Soft delete and restore. A restored row keeps its id, so links to it work again |
| The staged import wizard: stage a CSV, XLSX, JSON or XML file, a Google Sheets URL, or another table of the project; preview; start with a column mapping; poll until the status is terminal |
| Change table structure and its |
| Turn on double-booking protection (the one behaviour you set directly) |
Capture submissions through a form. Submitting it creates the underlying row, or a header row plus its line rows. Forms are the supported create path on the customer portal; the portal API has no endpoint that posts a raw row. See .
7. The API for customer apps
The customer-facing API lives under /api/portal/{projectSlug}/.... Do not hand-roll a client. Use @juxtaboard/portal-client: a typed TypeScript client with React Query hooks and optional Tailwind components, generated from the OpenAPI specification.
The specification is served without authentication at GET /api/portal/openapi.json. Point your own code generator at it.
Authenticate with Authorization: Bearer <jwt> from another origin, or with the portal_session cookie on the same origin. A staff app can use a team-app bearer token instead.
The package is a restricted package, not on the public npm registry. The compiled client is also served without authentication at GET /api/portal/sdk/bundle, and its component registry at GET /api/portal/sdk/registry.json.
To see which build of the client a deployment serves, call the MCP tool platform_describe with section: "integrationKit". It reports a version, which orders builds, and a revision, a digest of every shipped file, which identifies them. Compare versions across deployments and revisions within one deployment.
8. What you configure instead of coding
Most customisation is configuration on the tables the installer created.
For the meaning of each behaviour and the settings behind them, see and .