A numbered, copy-the-shape checklist for adding a new business module. Keep this open while wiring things up; jump to standards.md for the why behind each rule and to recipe.md for ready-to-paste starter files.
Replace <name> with your module's kebab-case singular name (e.g. ticket).
-
Create the backend four-file set under
apps/api/src/modules/<name>/:schema.ts,<name>.service.ts,<name>.routes.ts,index.ts.- Tables live in
schema.ts; the service consumesc.get("db"); routes wrapauthRequired;index.tsonly re-exports. - Every route must declare
describeRoute(...)and validate input withvalidator(...)from@/shared/lib/openapiso it lands in the OpenAPI spec. Why: openapi-standard.md. - Why: §2.1 File layout, §2.6 Schema sharding.
- Tables live in
-
Re-export the schema from
apps/api/src/db/schema.ts(one line, alphabetical):export * from "@/modules/<name>/schema";
Why: drizzle-kit walks this file when generating migrations. See §2.6.
-
Mount the routes in
apps/api/src/routes/protected.ts(one import + oneapp.routeline):import { <name>Routes } from "@/modules/<name>"; app.route("/", <name>Routes());
Use
public.ts/setup.tsonly when the route must work while the DB is locked. Why: §2.4 Route mounting. -
(Optional) Register a policy relation in
apps/api/src/modules/policy/namespace-config.tsonly if the sevenitemrelations (owner / editor / viewer / assignee / approver / watcher / parent_item) are insufficient. Add one entry inside the existing namespace'srelationsblock — do not create a new namespace lightly. Why: §0 Content modules. -
Register a backup contribution if the module owns persistent tables. Create
<name>.backup.tsexporting aBackupContribution, then inindex.ts:import { registerBackupContribution } from "@/modules/backup/registry"; import { <name>BackupContribution } from "./<name>.backup"; registerBackupContribution(<name>BackupContribution);
The import in
protected.ts(step 3) triggers this side effect. Why: §2.8 Backup contribution. -
Add the sidebar nav item. Create
apps/web/src/app/routes/_app/<area>/-<name>.nav.tsexporting aNavItem, then add one import + one array entry toapps/web/src/shared/components/sidebar/registry.ts. Why: §3.3 Sidebar and the "core principle" aggregate-file table. -
Add the i18n shard. Drop
apps/web/src/locales/en/<name>.jsonandapps/web/src/locales/zh/<name>.json; both must carry the same key set. The namespace list is derived automatically from the file system, so no edit toi18n.tsis needed. UseuseTranslation("<name>")in components. Why: §3.4 i18n sharding. -
Add tests. Unit:
apps/api/src/modules/<name>/<name>.test.ts(uses a temp SQLite per test). E2E: createtests/e2e/modules/<name>/with at least one*.test.tsper top-level resource, then append"<name>"toMODULE_DIRSintests/e2e/run.ts. Why: §5.0 Coverage philosophy and §5.3 e2e. -
Write the module doc
docs/modules/<name>.md(file layout, database, routes, auditing, end-to-end coverage, out-of-scope) and add one row todocs/architecture.md/docs/reference/api.md/docs/reference/database.md. Why: §4 Documentation sync. -
Generate the migration and run the gate.
bun run --filter @app/api db:generate # commit drizzle/<n>_*.sql + meta/_journal.json bun run check # lint + typecheck + test + build + check:i18n + check:env-docs + check:api-docs
All seven steps in
bun run checkmust be green before opening the PR. Why: §6 Quality gate, §8 Pre-merge checklist.