Skip to content

Authoring a module

Condensed walkthrough of adding a new module to Jarvis. Every command below is real and copy-pasteable, taken from the modules/sample/ module already in the repo.

The full guide

This page is a condensed version. For the complete, field-by-field walkthrough — including the CoreContext seam, the contributes.* hook, and recipes for adding a tab / an agent / a collection — read the full guide in the repo: docs/authoring-a-module.md.

What a module is

Jarvis has one hard invariant: adding a module never modifies the core. A module is a self-contained unit — its own manifest, its own database schema, its own optional background agent, its own optional Flutter tab — that plugs into the platform without anyone hand-editing core/, app/, or backend/ source.

That's enforced structurally: core/ never imports anything from modules/; only the thin app/ entrypoint depends on both core/ and whichever modules are enabled (listed in the root modules.yaml, not in code). Enabling a module runs a code generator instead of any hand-wiring — see Architecture for what it regenerates.

The contract

A module is a directory under modules/<name>/ with one required file and up to three optional pieces:

modules/<name>/
├── module.yaml         # manifest — the only required file
├── schema/
│   └── pb_migrations/  # PocketBase JS migrations, prefix-namespaced by module
├── agent/               # OPTIONAL — a Python job on the agent-runtime framework
│   └── agent.py
└── lib/                 # OPTIONAL — a Flutter package: the tab(s) this module adds
    └── <name>_module.dart

module.yaml declares the module's slug, title, identity tag (personal/freelance/rotary/shared), its Flutter surface (package, entry file, tabs variable), and an optional agent: entrypoint. It can also declare contributes.{obligations,entities}: true to say the module feeds one of the core's shared spine collections — a purely declarative hook that flows straight into the generated backend manifest.

Scaffold → enable → deploy

Run this from the repo root, through the ./jarvis wrapper (unrecognized verbs like module and deploy forward straight into the containerized jarvis CLI):

./jarvis module new sample --identity freelance
./jarvis module add sample
./jarvis deploy

jarvis module new <name> scaffolds the whole modules/<name>/ tree above — manifest, pubspec, surface tab, seed migration, agent stub, a Flutter test, analysis_options.yaml, .gitignore — filled in from <name> and --identity (default personal). It fails loudly if the directory already exists.

jarvis module add <name> does two things: it appends <name> to the enabled: list in the root modules.yaml, then regenerates four generated artifacts from the full set of enabled modules' manifests:

  • app/lib/generated/enabled_modules.dart — the Flutter tab registry.
  • backend/generated/manifest.json — the module manifest the backend and jarvis deploy read.
  • backend/generated/crontab — the scheduler's crontab, built from every enabled module's schedules:.
  • The managed block in app/pubspec.yaml — a local path dependency on each surfaced module's package.

You never hand-edit these four — they're build output, rewritten wholesale on every module add.

jarvis deploy is a P0 skeleton: it reads backend/generated/manifest.json, stages the baked migration list (core migrations first, then each enabled module's migrations), and echoes what a real deploy would do — a multi-arch image build/push and a cron install per module that declares an agent. It executes no Docker.

After module add sample, sample renders as a freelance-tagged tab in the running app — and none of core/, app/ source, or any other module's files changed.

Before you PR

This page covers the module side only. Before opening a PR, run the full monorepo gate and follow Conventional Commits and the one-slice-per-PR convention — see Dev setup & the gate for the exact commands and counts.