SANIRO MULTI-BRANCH ERP.
A custom ERP for a three-branch steel fabrication and roofing business. It replaced paper job cards with one system for orders, payments, branch stock and the workshop floor, so every sheet of steel and every hour of labour is tracked and billed correctly.
THE
ARCHITECTURE
Saniro ERP is a TypeScript monorepo (pnpm workspaces + Turborepo) with a NestJS API, a React 19 web app and a shared contract package. The shared package holds every enum, Zod schema, order state machine and error code, so the API and the UI can never drift apart.
The API is split into 16 domain modules (job cards, roofing orders, payments, ledger, inventory, stock transfers, price list, gate passes, workers, permissions and more) over a PostgreSQL schema of 37 tables. Each module follows controller, service and repository layers. Every input is validated with Zod at the boundary and every failure returns a stable error code.
A job card (the customer's order) holds many work orders, one per operation: cut, bend, pipe bending, sheet rolling or coil cutting. Each work order carries its measurements in a JSONB spec block and is priced from a central rate table. A line with no matching rate becomes a customized job that a supervisor prices by hand.
Seven roles, from cashier and supervisor to head-office auditor, see screens filtered to their branch and permissions. Queues update live over Server-Sent Events, and background jobs run on pg-boss inside Postgres, so PostgreSQL is the only infrastructure the system needs.
One database, no Redis. Background jobs use pg-boss in Postgres, live updates are in-process SSE and caching is in memory. For a business of this size that removes a whole class of moving parts while still meeting the under-3-second queue refresh target.
Money is integers, and the ledger is append-only. All amounts are stored as cents. A payment and its ledger entry are written in one transaction, and only the LedgerService may write to the ledger. A job card cannot close until its balance due is exactly zero.
Branch isolation is enforced on the server. The branch is never taken from the request body; it comes from the user's role. Head-office roles can read every branch, but any write they attempt on branch work is refused with a 403. Watching and transacting stay separate.
Concurrency is handled in the data layer. Every write sends the record's version (optimistic locking), create calls carry a client request ID so retries never create duplicates, and the database itself checks that stock can never go below zero.
Key
Technical Decisions
TECHNICAL
CHALLENGES
01. Stock Promised Twice
Stock was only checked when work started, so two paid orders could count on the same sheets. We added a reservation layer that holds stock as soon as an order is saved (available = on hand − reserved) and consumes the hold when the line starts. Shortages need an audited branch override. A backfill script and an integrity check (reserved = sum of open holds) moved existing orders over safely.
02. Rules That Disagreed
The approved prototype, the data model and the PRD's 26 business rules conflicted in several places, such as whether unpaid orders could reach the workshop. Each conflict was resolved with the product owner and written up as a numbered decision record, so both engineers built on the same rules and the reasoning is still there for whoever maintains the system next.
IMPACT & METRICS