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 Owner right on the project. Through MCP it needs the admin scope 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

list_process_modules

user

Lists the catalogue, optionally filtered by tier (foundation, domain, extension). Each entry has id, tier, provides, requires, enhances, tableCount, installed, installedPurposes and missingPurposes. installed is true only when the project holds a table for every purpose the module creates. A partial install shows as installed: false with a non-empty missingPurposes. The two site modules create no tables, so they always report installed: false

preview_module_install

user

Dry run, no writes. Input { moduleIds: string[] }. Returns { expandedModules, tables: [{ moduleId, templateId, purpose, action }], problems }

install_modules

admin

Input { moduleIds: string[], folderName?: string }. Creates the tables, binds foundations, wires cross-table references and runs each module's setup. Returns the created and bound table ids plus warnings. Without folderName the tables land at the project root

Careful: preview_module_install plans against an empty project. Its action is therefore always create, even for a foundation table the project already has. The real install binds that table instead. To see what the project already holds, read installed, installedPurposes and missingPurposes from list_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

moduleIds

string[]

Required, non-empty. Built-in module ids and the ids of packages imported into this project

parentId

number or null

Folder to install into. Default is the project root

folderName

string

Creates one folder with this name to hold the new tables. Bound (existing) tables stay where they are

seed

boolean

With verticalId, also writes the sample rows of that business type

verticalId

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.

•

Nothing is portal-exposed by an install. Share items with portal users explicitly afterwards. See .

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 POST /api/projects/{projectId}/modules/export

Body { folderId, moduleId?, name?, tier?, version? }. Returns { package, warnings }. The folder menu downloads <folder-name>.module.json and shows the warnings first (Export anyway). Defaults: moduleId is custom_ plus the name as lowercase letters, digits and underscores; tier is domain; version is 1.0.0. A moduleId you supply must match [a-z0-9_]+, otherwise the default is used

List packages of a project

GET /api/projects/{projectId}/modules

Returns { imported, official }

Import a package

Import module… in Set up from modules, or POST /api/projects/{projectId}/modules/import

Body is the package JSON. Returns { ok, moduleId, validation }. A failed validation returns 400 validation_failed with validation.errors. Importing the same moduleId again replaces the stored package

Remove an imported package

The trash icon on the module in the picker, or DELETE /api/projects/{projectId}/modules/{moduleId}

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

POST /api/projects/{projectId}/purchasing/receive

{ purchaseOrderDocumentId, purchaseOrderRowId }

Books every purchase-order line's ordered quantity into stock (plus rows in the movements ledger), then sets the order to received. Response { ok, stockReceived, linesReceived, linesSkipped }

POST /api/projects/{projectId}/pos/complete

{ saleDocumentId, saleRowId }

Issues every sale line's quantity out of stock (minus rows), then sets the sale to completed. Response { ok, stockIssued, linesIssued, linesSkipped }

POST /api/projects/{projectId}/production/complete

{ productionOrderDocumentId, productionOrderRowId }

In one operation, consumes each component of the bill of materials (minus rows) and produces the output (a plus row), scaled by qty_to_make divided by the BOM's yield. Then sets the order to completed. Response { ok, stockPosted, componentsIssued, componentsSkipped, outputProduced }

POST /api/projects/{projectId}/store-credit/apply

{ invoiceDocumentId, invoiceRowId, amount }

Draws amount from the customer's store-credit wallet and records a matching credit-applied payment on the invoice. The wallet cannot go below zero: an overspend is refused with 422 and nothing is written. Response { ok, payment, draw }

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

datatable_get_schema

Column ids, names and types, plus a write guide for shapes that need ids (people, groups, assets)

datatable_get_rows

Read rows with pagination, filtering and sorting

datatable_list_views

List saved views (grid, kanban, calendar and so on)

datatable_create_row

Create one row (data) or many (rows)

datatable_update_row

Partial update of one row or many: only the columns you send change

datatable_delete_row, datatable_restore_row

Soft delete and restore. A restored row keeps its id, so links to it work again

datatable_import_ingest, datatable_import_preview, datatable_import_start, datatable_import_status

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

datatable_set_meta

Change table structure and its purpose

datatable_set_behaviour

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.

•

Calls from a browser on another website work only if that website's address is on the project's CORS allowlist. Treat every entry as a trust grant (see and ).

•

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.

What you set

How

Outcome

Auto-numbered ids

A Sequence column (sequence) with a prefix such as INV-{YYYY}-

Gapless references (INV-2026-0042, PO-2026-0007) without code

Status workflows and auto-transitions

A Status column with allowed transitions, locks, required fields and conditional transitions

Records advance themselves on data changes or after a timeout. For example an unpaid invoice moves to its overdue state

Automations

Rules on a table: rollups, follow-up actions, scheduled jobs such as recurring invoices

Totals, reminders and recurring billing happen on their own. See

Engine behaviours

A table's purpose (ledger, price list, exchange rates, staff directory) and, directly, double-booking protection

A plain table gains running balances, price lookup or no-double-booking by configuration. Recurring billing is an automation action, not a behaviour

For the meaning of each behaviour and the settings behind them, see and .

· · · · ·