Building a module¶
A complete guide to developing a first-party Didacta module. The reference template is modules/hello-world — copy it and adapt.
Anatomy of a module¶
A first-party module is split into layers with clear responsibilities:
| Layer | Path | What it contains |
|---|---|---|
| Module package | modules/<slug>/ |
Pure domain logic: manifest, Zod schemas, services free of NestJS and Prisma. Testable in isolation. |
| NestJS host | apps/api/src/modules/<slug>/ |
Controllers, Prisma services, event bridges, workers. |
| Tables | packages/database/prisma/schema.prisma |
Models with @@map("mod_<slug>_…") + a versioned migration. |
| UI | apps/web/src/modules/<slug>/ + pages under apps/web/src/app/(app)/ |
The web extension (menu, settings tabs) and the Next.js pages. |
Minimum package structure¶
modules/my-module/
├── module.json # manifest for the linter (module-doctor)
├── package.json # @didacta/mod-my-module
├── README.md # with the 9 mandatory sections
├── tsconfig.json # extends ../../tsconfig.base.json
├── vitest.config.ts
├── src/
│ ├── manifest.ts # parseModuleManifest(...) → export const manifest
│ ├── service.ts # domain logic
│ └── index.ts # export const myModule: DidactaModule
└── tests/
└── contract.test.ts
The package's package.json (the pattern to copy):
{
"name": "@didacta/mod-mi-modulo",
"version": "1.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run"
},
"dependencies": { "@didacta/core-kernel": "workspace:*" },
"devDependencies": { "@didacta/core-registry": "workspace:*", "vitest": "^2.1.8", "typescript": "^5.7.2" }
}
Full checklist¶
- Create
modules/<slug>/with the structure above (the pnpm workspace already includesmodules/*). - Write the manifest in
src/manifest.tswithparseModuleManifest(...). - Export the
DidactaModulecontract fromsrc/index.ts. - Register it in the
registry.register([...])array inapps/api/src/modules/module-registry.service.ts(order does not matter: topological ordering is automatic). - Create the NestJS host in
apps/api/src/modules/<slug>/and declare it inapps/api/src/modules/modules.module.ts. - Add the tables to the Prisma schema with the
mod_<slug>_prefix +tenant_id, and generate the versioned migration. - Create the UI extension in
apps/web/src/modules/<slug>/index.tsand register it inapps/web/src/modules/index.ts. - Complete
module.jsonand theREADME.mdwith the 9 sections, and pass the validations.
The golden rules¶
From the module contract (full rules):
- Never import another module's code.
- Never write to another module's tables (reading is fine, declaring the dependency and filtering by
tenant_id). - Never modify the core for one of your module's features.
- Never emit events without declaring them in the manifest.
- Never create foreign keys pointing at another module's tables.
- Never put business logic in controllers.
- Never gate a module behind a license: every module is Community.
Step-by-step guide¶
- The manifest — the module's declarative contract.
- Database — tables, RLS and migrations.
- Backend — the
DidactaModule, theModuleContextand the NestJS host. - User interface — menu, pages and settings tabs.
- Events and hooks — communicating with other modules.
- Validation and tests — module-doctor, the README and the test suites.