RMC ERP
A single-codebase ERP for ready-mixed-concrete plants — order-to-cash, procure-to-pay, batch production, IS-standard lab testing, real-time dispatch, and GST-compliant accounting. 116 data models, built end-to-end, solo.
Context
A ready-mixed-concrete plant is a surprising amount of software hiding behind a batching drum. Sales cut quotations, the plant floor runs mix designs and batches, a lab certifies cube strength, vehicles dispatch challans, and accounts reconcile it all against GST — and in most plants those live in five disconnected tools, or a stack of paper registers.
RMC is the attempt to fold all of that into one system, so a job can be traced from a customer inquiry through production, lab certification, delivery challan, e-invoice, and payment against a single ledger. Built like a product, not an internal script.
Architecture
One Turborepo monorepo, pnpm workspaces. A NestJS 11 API (apps/api), a Next.js 16 App-Router web client (apps/web), and shared packages — a generated Prisma client, Zod-backed shared types, and finance/date utilities — so a validation shape is written once and enforced on both sides.
The API is modular: roughly two-dozen feature modules (auth, masters, accounting, inventory, sales, purchase, production, lab, vehicle, dispatch, reports). Auth is hand-rolled — short-lived JWT access tokens with rotating refresh tokens, argon2 hashing, refresh sessions in Postgres and Redis, and an access-token blacklist in Redis on logout.
The data model
The heart of it is the schema — 116 Prisma models across fifteen domain files, no enums faked as loose strings where a relationship was the honest answer. RBAC is module.action permissions resolved through a guard and cached per-user in Redis, with multi-company / multi-branch tenancy carried on the records themselves.
Dispatch runs live: a Socket.io gateway on a /dispatch namespace pushes status changes, new schedules, and challan updates to the floor without a refresh. Invoices, registers, and lab certificates render to PDF through Puppeteer; lists export to Excel.
The domain is the hard part
The interesting complexity wasn’t the CRUD — it was encoding the domain honestly. IS 383 sieve analysis and 7/28-day cube-strength tracking in the lab module. GST sales invoices with credit and debit notes, TDS, and GSTR-shaped accounting. Mix-design approval flows before a grade can be produced. Getting those right is what separates a demo from something a plant would actually run its money through.
Decisions & tradeoffs
- Hand-rolled auth over a hosted provider. An ERP needs a real permission matrix and audit trail, not a magic-link UX. Rotation and Redis blacklisting were worth owning.
- Shared types package, not two schemas. Zod shapes in a shared package meant the API and the Ant Design forms never drifted — the tax on a monorepo paid for itself by week two.
- Strings over Prisma enums for status. A deliberate convention — migrations stay cheap when a new status is a value, not a schema change. The cost is discipline at the boundary.
State & what I would change
It runs the full order-to-cash and procure-to-pay cycle today, seeded against a realistic plant. If I were starting over I’d pull reporting into its own read-optimised path earlier — Puppeteer PDF generation in the request cycle is fine until a month-end register is 400 pages. And a job queue for the email worker from day one, rather than the cron I bolted on.