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.
| half | import | holds |
|---|---|---|
| manifest and collections | locosdk/schema | the builder: its collections under the app's names, its rule slots, its options, its catalogue entry, its version and steps |
| server | locosdk/server | one bind call: hooks, routes, crons, MCP tools |
| screens | locosdk/ui | a 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 leftnullis superusers only. - Links between modules are typed options.
quotes({ crm: … })names crm's collections in this app,stock({ site })scopes stock by thesitesinstall. 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 asnumbers. defineSchemarefuses 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
stepsare 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'sdefaults, written once, only into an empty collection.
The golo commands#
| command | what |
|---|---|
golo modules [--json] | what the app installs: each module, its version and options, then every collection and its owner |
golo modules check | every 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.
| module | schema | server | screens | recipe |
|---|---|---|---|---|
crm | crm(...) | none | CrmScreens | yes |
quotes | quotes(...), quotesSeries() | bindQuotes(app) | QuotesScreens | yes, requires numbering |
assets | assets(...) | bindAssets(app) | AssetsScreens | yes |
stock | stock(...), stockSeries() | bindStock(app) | StockScreens | yes, requires numbering |
workorders | workorders(...), workordersSeries() | bindWorkorders(app) | WorkordersScreens | yes, requires numbering and stock |
purchasing | purchasing(...), purchasingSeries() | bindPurchasing(app) | PurchasingScreens | yes, requires numbering |
planning | planning(...) | bindPlanning(app) | PlanningScreens | yes |
claims | claims(...) | bindClaims(app) | ClaimsScreens | yes, requires numbering, crm and assets |
chase | chase(...) | bindChase(app) | ChaseScreens | yes |
attachments | attachments(...) | bindAttachments(app) | AttachmentsScreens | yes |
rights | rights(...) | guardRights(app, access) | RightsPage | yes |
sites | sites(...) | guardSites(app, site) | SitesPage | yes |
users | accounts(...) | guardUsers(app, team) | UsersPage | yes |
numbering | numbering(...) | mintNumbers(app) | none | yes |
lists | lists(...) | guardLists(app) | ListsPage | yes |
staged | staged(...) | stageWrites(app) | ReviewQueue, ProposalPanel | yes |
inbox | inbox(...) | readInbox(app, …) | InboxScreen | yes, requires staged; each inbox's reader is code |
audit | auditLog(...) | journal(app, …) | AuditJournal, RecordHistory | yes |
views | savedViews(...) | none | SavedViews, ViewsMenu, PinnedViews | yes |
bugs | bugReports(...) | none | BugReportButton, BugReportsGrid | yes |
documents | none | none | the A4 and receipt renderer, VAT per rate, French mentions | no: screens only |
mail | none | letterhead, mailRoute, mailTransport | openMailCompose | no: server only |
ask | none | askBox(app, …) | AskButton | no: server only |
dashboard | none | dashboardRoute | KpiTile, ChartCard, DashboardFrame | no |
kit | none | none | LinesEditor, StatusMenu, the timeline | no: screens only |
A module with no recipe is wired by an agent, on a request.