DocsDevelopers

Modules in code

What a module is in locosdk, how an app installs one (the spread in schema.ts is the install), the recipes behind the portal's one click install, and the catalogue.

A module is a name shared by up to three halves of locosdk, plus a manifest. It ships inside the sdk, carries an integer version, and holds a contract that the app's gate runs. The business view of the same modules: Modules.

halfimportholds
manifest and collectionslocosdk/schemathe builder: its collections under the app's names, its rule slots, its options, its catalogue entry, its version and steps
serverlocosdk/serverone bind call: hooks, routes, crons, MCP tools
screenslocosdk/uia screens object with its pages, components, its strings in English and French

The halves stay in their tiers, so a browser bundle never imports a server half.

The spread is the install#

// schema.ts
import { auth, crm, defineSchema, numbering, quotes, quotesSeries, text } from 'locosdk/schema'

const staff = '@request.auth.id != ""'

export const numbers = numbering({ readRule: staff, timeZone: 'Europe/Paris', series: { ...quotesSeries() } })

export const schema = defineSchema({
    users: auth({ fields: { name: text() } }),
    ...numbers.collections,
    ...crm({ users: 'users', listRule: staff, createRule: staff, deleteRule: null }).collections,
    ...quotes({ users: 'users', listRule: staff, createRule: staff, deleteRule: null, crm: { companies: 'companies', contacts: 'contacts', deals: 'deals' }, numbers }).collections,
})
// server.ts, before app.serve()
mintNumbers(app)
bindQuotes(app)
// src/client.ts and src/main.tsx: the screens, mounted by the app
export const crmUi = new CrmScreens(installOf(schema, 'crm'), { db, session })
// in the pages of the AppRouter
...crmUi.pages({ group: navGroup('customers') })
  • Rules are the module's law. What the app decides arrives through rule slots (listRule, createRule, deleteRule, and the like) filled with the app's own snippets. A slot left null is superusers only.
  • Links between modules are typed options. quotes({ crm: … }) names crm's collections in this app, stock({ site }) scopes stock by the sites install. A typo in a target fails the typecheck.
  • One numbering per app. Every numbered module joins its series into the one numbering(...) install and takes it as numbers.
  • defineSchema refuses a module collection missing or replaced by an app spec, a module installed twice, a key the module did not name, and a field its snippets read that the app lacks. Each refusal names the way out.
  • serve() refuses an app that holds a module with a server half it never bound.
  • Each module's collections are compiled against the composed schema at boot, so a bad slot fails the boot, not the first request.

Customize through the options#

A module grows through its extension points: rule slots, names (its collections under the app's words), fields (app fields on a module collection, typed into the records), the app's own hooks chained around the module's, the screens' props and slots, and string overlays through uiI18n.configure. A client need that no point covers becomes a new point in the sdk, so every app keeps getting the module's fixes through its sdk update and its gate.

Versions and migrations#

  • A module's shape is its current collections. Its steps are deltas between two of its versions, recorded as <module>@<n> in _migrations.
  • A module none of whose collections exists yet (a fresh database, or a module just added to a live app) is built from its current shape, and every step is recorded as applied.
  • At boot: each module's unrecorded steps, then the app's migration files, then the sync.
  • Rows a new database needs (the default Admin profile of rights) are the module's defaults, written once, only into an empty collection.

The golo commands#

commandwhat
golo modules [--json]what the app installs: each module, its version and options, then every collection and its owner
golo modules checkevery installed module's contract, on an in memory copy of the schema. golo gate runs it as a step
golo modules install <name> [--set key=value …] [--write]the module's recipe on the app's files: the diff, written with --write
golo modules uninstall <name> [--write]the module taken out of the app's files, its tables dropped by a migration

Recipes: the one click install#

A module that the portal installs carries a recipe: the deterministic edits its install makes from the owner's answers, never a model. It asks a few typed questions (an auth collection, a rule, some of the app's collections, a choice, another module to link), then edits schema.ts (imports, a declared install, the entries, the accounts collection when the module adds fields to it), server.ts (the bind call before app.serve()), src/client.ts and src/main.tsx (the screens and their menu entries), and adds a new: line under ## Unreleased of CHANGELOG.md in the app's language.

The portal shows the diff, then applies that exact plan: the dev app stops, the files are written, golo gate runs, the dev app starts again on its new tables, and the change is committed and pushed. A red gate or a failed boot restores every file and the database as they were. Uninstall is the inverse, found from the recipe's own import names; it is refused while another module requires this one, or while the app's own code still names its screens.

The catalogue#

Each module has its own page in the agent kit, inside the app: node_modules/locosdk/agent/api/modules/<name>.md.

moduleschemaserverscreensrecipe
crmcrm(...)noneCrmScreensyes
quotesquotes(...), quotesSeries()bindQuotes(app)QuotesScreensyes, requires numbering
assetsassets(...)bindAssets(app)AssetsScreensyes
stockstock(...), stockSeries()bindStock(app)StockScreensyes, requires numbering
workordersworkorders(...), workordersSeries()bindWorkorders(app)WorkordersScreensyes, requires numbering and stock
purchasingpurchasing(...), purchasingSeries()bindPurchasing(app)PurchasingScreensyes, requires numbering
planningplanning(...)bindPlanning(app)PlanningScreensyes
claimsclaims(...)bindClaims(app)ClaimsScreensyes, requires numbering, crm and assets
chasechase(...)bindChase(app)ChaseScreensyes
attachmentsattachments(...)bindAttachments(app)AttachmentsScreensyes
rightsrights(...)guardRights(app, access)RightsPageyes
sitessites(...)guardSites(app, site)SitesPageyes
usersaccounts(...)guardUsers(app, team)UsersPageyes
numberingnumbering(...)mintNumbers(app)noneyes
listslists(...)guardLists(app)ListsPageyes
stagedstaged(...)stageWrites(app)ReviewQueue, ProposalPanelyes
inboxinbox(...)readInbox(app, …)InboxScreenyes, requires staged; each inbox's reader is code
auditauditLog(...)journal(app, …)AuditJournal, RecordHistoryyes
viewssavedViews(...)noneSavedViews, ViewsMenu, PinnedViewsyes
bugsbugReports(...)noneBugReportButton, BugReportsGridyes
documentsnonenonethe A4 and receipt renderer, VAT per rate, French mentionsno: screens only
mailnoneletterhead, mailRoute, mailTransportopenMailComposeno: server only
asknoneaskBox(app, …)AskButtonno: server only
dashboardnonedashboardRouteKpiTile, ChartCard, DashboardFrameno
kitnonenoneLinesEditor, StatusMenu, the timelineno: screens only

A module with no recipe is wired by an agent, on a request.

This page as markdown