BuildLedger Construction ERP
A production-ready construction ERP for projects, materials, labour, vendors, expenses, client payments, and invoices. I built the backend financial engines for cash flow, receivables and payables, and project net positions, plus inter-project fund transfers and settlements that track which projects owe money without double-counting internal transfers.
- React
- TypeScript
- Node.js
- Express.js
- Prisma
- PostgreSQL
- React Query
- JWT
- Cloudflare R2
- Tailwind CSS
Private project, demo on request
This is client or employer work, so the source code is confidential and the live app sits behind a login. I can't share screenshots or credentials publicly, but I'm happy to walk you through it on a call: a live demo, the architecture and selected code.
Architecture

Overview
BuildLedger is a single-tenant construction ERP: a React SPA talking to a versioned REST API (/api/v1) backed by PostgreSQL. The backend is a modular monolith with 17 domain modules (projects, materials, labour, vendors, expenses, client payments, invoices, salary slips, fund transfers, documents, reports, users, audit, deletion requests and more), each split into the same layers:
routes → controller → service → repository → Prisma → PostgreSQL- Routes attach auth, permission checks and Zod validation
- Controllers translate HTTP into service calls
- Services own business rules (who can void, how settlement works)
- Repositories are the only layer that touches Prisma
System
React + TypeScript (Vercel)
│ React Query cache · JWT access token · httpOnly refresh cookie
▼
Express API (Railway)
│ helmet → request timing → pino logging → CORS → rate limits
│ → auth → permissions → validation → module
├──► PostgreSQL (Prisma ORM, NUMERIC(14,2) money columns)
└──► Cloudflare R2 (documents via presigned URLs)Key design decisions
Money is never a float. All money columns are NUMERIC(14,2), and invoice and payment math goes through a shared Decimal helper that rounds once, half-up, to 2 decimal places. Floating-point drift (0.1 + 0.2 ≠ 0.3) never reaches a printed invoice.
Inter-project transfers without double counting. When one project funds another, the money moves between projects; it isn't company income or expense. The financial engine nets every pair of projects into a single balance (A→B minus B→A), so each project sees a clear receivable (they owe us) or payable (we owe them). Internal transfers never inflate company-wide totals. Net position = available cash + receivables − payables.
Financial records are voided, not deleted. Transfers and entries are voided with a reason, who did it and when. Real deletions go through an approval workflow: a user requests, a reviewer approves or rejects, and a snapshot of the record is kept so it can be restored.
Everything is audited. A Prisma middleware records every write (old value, new value, who, when) in an audit log, using the request context so no module has to remember to do it.
Access is scoped per project. Admins and accountants see everything. Other roles see only projects granted to them, and that list is read from the database (cached for 60s), not from the JWT. Removing someone's access takes effect within a minute, not when their token expires.
Defense in depth on writes. Global rate limiting, plus a stricter limiter on financial write routes (payments, materials, labour, vendors, expenses, transfers).
Quality
- Vitest + Supertest tests for money math, invoice totals, project scoping (including IDOR checks), auth, uploads, and race conditions in deletion review
- OpenAPI docs generated with Swagger
- Request timing and structured logs with request IDs for tracing slow calls
Key Implementation Highlights
Built the project financial engine: cash in hand, receivables, payables and net position per project
FinancePairwise settlement of inter-project fund transfers without double-counting internal money movement
FinanceExact money arithmetic with NUMERIC(14,2) and a Decimal helper instead of floats
DataPer-project access scoping read from the DB, so revoked access applies within 60s, with IDOR tests
SecurityJWT access tokens with rotating refresh tokens in httpOnly cookies, plus rate limits on financial writes
SecurityAutomatic audit log of every write via Prisma middleware, with old and new values
AuditVoid-with-reason for financial records and an approval workflow for deletions, with snapshot restore
WorkflowModular monolith: 17 domain modules with a strict routes → controller → service → repository layering
ArchitectureDocument uploads to Cloudflare R2 using presigned URLs
StorageVitest + Supertest suite covering money math, scoping, auth and concurrent deletion reviews
Testing
Interested in the technical details?
Let's Talk