200OK
Agency operations platform with Malaysian payroll
Projects, live task boards, invoices and Malaysian statutory payroll in one platform, built for an IT agency's own work.
- Kind
- Internal operations and payroll
- Our part
- Our own platform, designed and built by 404 Tech for our own work.
404 Not Found
What was missing
An IT agency (ours) ran projects, tasks, client feedback, estimates, invoices, claims, assets, domains, deployments and staff in separate tools. Nothing joined up, and payroll sat outside all of it. What was missing was one platform: sub-projects, a live task board, documents a client can open from a link, and a payroll module that works out Malaysian statutory contributions and issues payslips.
Constraints
What the system had to hold to, before a line was written.
- Statute
- Statutory tables: EPF Third Schedule (October 2025), SOCSO under the PERKESO Act 4 including SKBBK, and EIS under Act 800.
- Records
- A finalised payroll run never reopens. Staff identity fields are encrypted at rest.
- Production
- Production is live: every change must be non-breaking, and the schema is applied by its owner.
- Rate limit
- A global limit of 120 requests a minute, per handler, per user rules out polling.
- Access
- Roles are data, with page access set per role.
The system
A Nuxt app, plus public token pages for shareable, revocable invoices and estimates, talks to a NestJS REST API. Prisma maps it to MySQL. Project boards update over server-sent events: one stream per project, behind a stream-access guard. The payroll engine is a pure calculator, with no database and no clock. It takes plain data and returns payslip lines plus blocking and non-blocking issues. The service around it loads the band tables, writes the results and hands payslips to a PDF renderer. Staff identity fields pass through AES-256-GCM encryption on write and decryption on read. Integrations cover source control, deployments, webhooks, domain expiry and object storage.
App and token pages → REST API → MySQL. Board changes push back over server-sent events; payroll runs through a pure calculator before anything is written.
- Built by us
- Data held
- Third party
- Offline or out of band
Read the drawing as text
Parts
- 01Web app: projects, finance, HR
- 02Token pages: revocable share links
- 03REST API: JWT, throttled
- 04Event streams: one per project
- 05Database: MySQL via Prisma
- 06Payroll calculator: no database, no clock
- 07PDF renderer: payslips, invoices
- 08Field encryption: AES-256-GCM
- 09Integrations: source, deploys, domains
Flows
- Web app to REST API: REST
- Token pages to REST API: token-scoped read
- REST API to Event streams: board change
- Event streams to Web app: server-sent events (alternate or out of band)
- REST API to Database: queries
- REST API to Payroll calculator: plain data
- Payroll calculator to Database: lines, issues
- Payroll calculator to PDF renderer: payslip
- REST API to Field encryption: staff fields
- Field encryption to Database: ciphertext
- Integrations to REST API: webhooks in, calls out (alternate or out of band)
The screen
Most of this system sits behind a login, so its key screen is redrawn here as a schematic. Bars stand in for every name and figure.
Decisions
The engineering calls that shaped the build, and why each one was made.
Money in integer cents, end to end.
Every amount is stored and moved as minor units. Conversion happens only where a person types or reads a figure.
State from timestamps, not a status column.
A payroll run is a draft until its finalised time is set, and paid once its paid time is. There is no second copy of the truth to fall out of step.
Pure arithmetic, separate from IO.
The calculator never touches the database or the clock, so the statutory maths is tested on fixtures.
Rates are editable band tables.
Contribution rates live in data, not code. Non-standard schemes switch on only with an explicit director tick, and income tax (PCB) is entered rather than computed.
Server-sent events, not polling.
The rate limit made polling impossible. The board now receives changes as they happen.
200OK
Shipped
One platform for projects, finance, HR and payroll. Sub-projects nest one level deep, and merge or delete is refused while a project has children. Payroll runs prorate by calendar day, allow backdated payslips, export to CSV and print payslip PDFs. Invoices and estimates go out as links the agency can revoke.
| Figure | What it counts |
|---|---|
| 3 | statutory schemes (EPF, SOCSO, EIS) from versioned band tables |
| 69 | data models |
| ~333 | API route handlers across 48 controllers |
| 87 | test spec files, 16 of them for HR and payroll |
| 51 | page files in the web app |
Built with
- NestJS
- Prisma
- MySQL
- Nuxt 4
- Tailwind CSS
- shadcn-nuxt
- reka-ui
- PDFKit
- Server-sent events
- Swagger
- Jest