Maintenance Management (CMMS)
Multi-company maintenance flow: fault → SLA → work order → field → closure.
How this was measured: vitest + a separate integration config. Layer rules are checked by dependency-cruiser on every build.
- APPS
- api
- web
- saha
- admin
- PACKAGES
- core
- db
- shared
- ui
- config
- RULES
- dependency-cruiser5 layer rules
- DATA
- PostgreSQL
Problem
Maintenance runs on memory: a fault is reported verbally, nobody records who intervened when, which part was used, and whether the machine actually returned to production. The same fault recurs and the cost stays invisible.
The second dimension is multi-company. This was built as a product to be sold to companies, not for one customer, so isolation and permission had to be right from day one.
Constraints
- The same business rules are called from the HTTP API and from a queue consumer; rules cannot live in a controller.
- The field app must work offline and sync later.
- Identity and permissions are delegated to Keycloak, but each module keeps its own boundary.
Architecture decisions
Four processes, one business core
apps/api, apps/worker, apps/web, apps/mobile; business modules in packages/backend, so the same rule is not written twice.
Cost: Two processes deployed separately; deployment complexity.
Three layers per module, one public surface
application/ holds flow and transaction boundary, domain/ is pure TypeScript, infrastructure/ holds Prisma and adapters. Modules talk only through index.ts.
Cost: A small change touches three files; in return cross-module writes to another module's table are impossible.
Layer boundary in CI, not in docs
Five rules checked by dependency-cruiser: domain imports no Nest/Prisma/HTTP, no @prisma/client outside infrastructure, rules do not attach to HTTP objects, modules use only index.ts, no reversed layer direction.
Cost: A new developer hits the wall on day one; a week later they defend the rule.
Client generated, not hand-written
The api-client package is generated from the API's OpenAPI output; contract drift is caught in CI so the client never goes stale silently.
Cost: A generation step in the build.
Side effects through outbox + queue
Notifications and integrations are not done inside the HTTP request; an outbox is written and a worker consumes it via BullMQ.
Cost: At-least-once delivery; consumers must be idempotent.
Proof
- Test files68 (vitest + integration)
- Apps / packages4 / 5
- Enforced layer rules5, on every build
- API contractclient generated from OpenAPI, drift caught in CI
Known limits
- Not piloted on site; production hardening is open.
- Source is in a private repo.
What I learned
A layer rule left in a document rots. The day you move it into CI the argument ends: the violation shows up in a red build, not in code review.