Skip to content
Ercan Labs
← All workPLATFORM · PRIVATE · in-development

Maintenance Management (CMMS)

Multi-company maintenance flow: fault → SLA → work order → field → closure.

68
Test files
4
Apps
5
Packages
5
Enforced layer rules

How this was measured: vitest + a separate integration config. Layer rules are checked by dependency-cruiser on every build.

Architecturelayer rules run on every build
  1. APPS
    • api
    • web
    • saha
    • admin
  2. PACKAGES
    • core
    • db
    • shared
    • ui
    • config
  3. RULES
    • dependency-cruiser5 layer rules
  4. DATA
    • PostgreSQL
Business rules live in core; the api package only transports. A rule breach fails the build.

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

01

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.

02

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.

03

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.

04

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.

05

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.

Stack

NestJSPrismaPostgreSQLBullMQReactReact NativeKeycloak