Skip to main content

Writing a module

When a bundle or custom app isn't enough, you can write a module. A module adds a new capability to Cobblr: its own item type, its fields, its screens, and its behaviors. It's the same unit the built-in features are made of.

What a module declares

A module is mostly a declaration:

  • the item type it manages and the fields those items carry
  • how it appears (a nav entry and screens), if it's a domain module
  • what it contributes to the rest of the app, like scan routing or default views
  • whether it can be instanced more than once

Modules stay ignorant of each other. Yours composes through the same shared primitives (items, fields, views, events) the others use, so it drops in without touching anything already there.

Isolation

A module can read across to another's data only through explicit, granted paths, never by reaching in directly. This keeps the system safe to extend: a new module can't quietly break an existing one, and it doesn't have to know anything about the modules already there.

Three decisions before any code

They're cheap to make and expensive to undo, because the name threads through every table, event, and route:

  1. Capability or domain? A thing users manage (a noun, a table, a nav entry) is a domain with a bare name, opt-in. Plumbing that works on whatever exists is a capability, named core-<thing>, usually auto-enabled. The module system covers why the name is load-bearing.
  2. Band. foundational is reserved for "the platform can't work without it"; almost everything is stock (in the box, toggleable).
  3. Instanceability. single by default; multi if one workspace plausibly wants two separate collections of the same shape (two inventories, say).

The manifest

A module is a package at modules/<name> whose heart is one declaration:

  • identity: name, band, whether it auto-enables, instanceability.
  • schema: its table prefix and where its migrations live. Every table it owns starts with <name>_.
  • provides: the entity kinds it manages, each with its fields; a field can carry a role (title, quantity, image) so the rest of the platform knows how to treat it, and only declared fields can be exposed to views and read scopes.
  • exposes: the events it announces (<name>.<noun>.<verb>) and the actions it offers (<name>:<verb-noun>), which is everything wires and other modules can see of it.
  • api and optional ui entry points the platform loads.

The manifest is validated at boot, strictly: reference a field you didn't declare and the module refuses to load rather than half-work.

Migrations that self-heal

A module owns its migrations, and the platform runs them wherever they're needed: on install, on upgrade, and on workspaces that existed before your module did. The bar is that an upgrade never requires a manual step, ever; see the contribution guide for the full rule.

Verifying it

Typecheck and build, then prove it where it runs: enable the module in a fresh workspace and exercise it end to end. Fresh workspaces don't enable optional modules on their own, so an e2e test enables yours first, then drives the UI. The module-authoring checklist in the repo walks every wiring step.