All work

Systems & Platforms

PeopleOS

HR, attendance and statutory payroll at a ~300-employee manufacturer

2026Production

At a glance

Credit

Built by
Denandro YusufArchitecture · Domain and rule design · Implementation · Statutory rule interpretation · Release decision · Directing Claude Code against written specs · Review of every later change

Run the demo synthetic data

Problem
HR lived in a 291-column workbook, paper forms and a macro-driven payroll spreadsheet.
What I did
I built one platform: an append-only attendance ledger and versioned payroll rule engines.
Outcome
One auditable system of record, live since August 2026. Phased rollout toward a ~300-employee target.

Explore

The component graph, from the source repository. Follow the arrows: left to right, and down where two steps share a column. The burgundy bars are where decisions are made; the numbers on the lines are the key flows, written out under the graph.

PeopleOS: architectureData flows through 12 components: PWA client (Interface: Admin / Manager / Employee workspaces); Next.js 16 App Router (Service: RSC + 35 server-action modules at v9.30, 36 since v10.1.0); Legacy workbooks (Source: 291-column master data, tax table); CLI: import/reconcile (Ingest: Integrity checks, environment diffs); Permission resolver (Decision logic: 158 codes at v9.30, 161 since v10.1.0 · level + grants − restrictions); Cloud Run + Cloud SQL (External: Non-destructive, code-only releases); Domain modules (Decision logic: 17 at v9.30, 18 since v10.1.0 · authorize, scope and audit every action); Pure rule engines (Decision logic: Attendance day · overtime · BPJS · PPh 21 · THR); Field encryption (Decision logic: AES-256-GCM, versioned ciphertext); Document store (Store: Magic-byte validated, versioned); Payslips · bank file (Output: React-PDF · exceljs · XLSM bank template); PostgreSQL (Store: 102 models · 29 migrations at v9.30 · event ledger). Connections: PWA client to Next.js 16 App Router (server actions); Next.js 16 App Router to Permission resolver (resolve effective permissions); Permission resolver to Domain modules (permission + scope); Domain modules to Pure rule engines (buckets → priced results); Domain modules to Field encryption (encrypt / mask); Domain modules to PostgreSQL (reads, writes, audit events); Field encryption to PostgreSQL (ciphertext columns); Domain modules to Document store; Domain modules to Payslips · bank file (generate); Legacy workbooks to CLI: import/reconcile (workbook parsing); CLI: import/reconcile to PostgreSQL (import / reconcile / verify); Next.js 16 App Router to Cloud Run + Cloud SQL (container); Cloud Run + Cloud SQL to PostgreSQL (Cloud SQL socket).PWA client — Admin / Manager / Employee workspacesPWA clientAdmin / Manager /Employee workspacesNext.js 16 App Router — RSC + 35 server-action modules at v9.30, 36 since v10.1.0Next.js 16 AppRouterRSC + 35 server-actionmodules at v9.30, 36since v10.1.0Legacy workbooks — 291-column master data, tax tableLegacy workbooks291-column masterdata, tax tableCLI: import/reconcile — Integrity checks, environment diffsCLI:import/reconcileIntegrity checks,environment diffsPermission resolver — 158 codes at v9.30, 161 since v10.1.0 · level + grants − restrictionsPermission resolver158 codes at v9.30,161 since v10.1.0 ·level + grants −restrictionsCloud Run + Cloud SQL — Non-destructive, code-only releasesCloud Run + CloudSQLNon-destructive,code-only releasesDomain modules — 17 at v9.30, 18 since v10.1.0 · authorize, scope and audit every actionDomain modules17 at v9.30, 18 sincev10.1.0 · authorize,scope and audit everyactionPure rule engines — Attendance day · overtime · BPJS · PPh 21 · THRPure rule enginesAttendance day ·overtime · BPJS · PPh21 · THRField encryption — AES-256-GCM, versioned ciphertextField encryptionAES-256-GCM, versionedciphertextDocument store — Magic-byte validated, versionedDocument storeMagic-byte validated,versionedPayslips · bank file — React-PDF · exceljs · XLSM bank templatePayslips · bankfileReact-PDF · exceljs ·XLSM bank templatePostgreSQL — 102 models · 29 migrations at v9.30 · event ledgerPostgreSQL102 models · 29migrations at v9.30 ·event ledger123456789101112

Key flows

  1. PWA client to Next.js 16 App Router: server actions
  2. Next.js 16 App Router to Permission resolver: resolve effective permissions
  3. Permission resolver to Domain modules: permission + scope
  4. Domain modules to Pure rule engines: buckets → priced results
  5. Domain modules to Field encryption: encrypt / mask
  6. Domain modules to PostgreSQL: reads, writes, audit events
  7. Field encryption to PostgreSQL: ciphertext columns
  8. Domain modules to Payslips · bank file: generate
  9. Legacy workbooks to CLI: import/reconcile: workbook parsing
  10. CLI: import/reconcile to PostgreSQL: import / reconcile / verify
  11. Next.js 16 App Router to Cloud Run + Cloud SQL: container
  12. Cloud Run + Cloud SQL to PostgreSQL: Cloud SQL socket

Go deeper

The full account

Problem
HR ran on a 291-column master workbook, a legacy attendance app, paper leave forms and a macro-driven payroll spreadsheet. Nobody could answer who reports to whom, on what contract, since when — and no month's payroll could be proved to have come from a specific set of rules.
What was built
A full HR platform, at the August 2026 cutover: 17 domain modules over a 102-model Postgres schema, an append-only attendance ledger, and separately versioned rule engines that price statutory payroll in exact decimal arithmetic.
Role
Built hands-on, directing Claude Code against my own written specifications, and acted as de-facto product owner: domain model, rule interpretation with Accounting and HR, release process, and production ownership. Since September Prada Dipa, an engineer on my team, has helped maintain and develop it under my direction, and every change needs my review.
What changed
Replaced spreadsheets and paper with one auditable system of record, live on managed cloud infrastructure since the August 2026 cutover. The rollout is phased: ~300 is the company's headcount and the platform's design target, not the number of people on the system today.

Built hands-on, directing Claude Code against written specifications while owning the architecture, the rule interpretation, the safety regime and the release decision.

Context

The company is an Indonesian apparel manufacturer of roughly 300 staff — Bali timezone, IDR, Bahasa Indonesia primary with an English fallback. HR operations lived across a master-data workbook, chat threads, paper forms and a payroll spreadsheet whose macros nobody wanted to touch.

Two forces made the spreadsheet stack untenable. Indonesian statutory payroll — PPh 21, BPJS contributions, overtime multipliers and THR, plus the company's own 78-hour monthly overtime threshold — was computed by hand every month with no record of which rule produced which payslip. And Indonesia's personal data law (UU 27/2022) raised the bar for demonstrable access control over employee data, which a shared workbook fundamentally cannot provide.

The brief was not 'build an HR app'. It was: make each month's payroll reproducible, defensible, and impossible to quietly alter after the fact.

Architecture and the system

A Next.js 16 App Router monolith. At the August 2026 cutover every mutation was a server action rather than a REST endpoint: 108 page routes, and the only route handlers were read-only downloads. Four authenticated endpoints write today besides the server actions: one for screening results and three for HR reminders. Authorization resolves before any module runs; pure rule engines sit beneath the modules and never touch I/O.

Attendance as an event ledger, not a timesheet

Attendance is stored append-only. Punches, corrections and voids are events; nothing is ever edited in place. A correction is a void-and-replace pair with its own audit record, so a dispute six months later can be reconstructed exactly as it happened.

A pure day engine turns that ledger into priced minute buckets: events plus schedule plus leave plus rules in, buckets out. No I/O, no clock, no database — which is what makes it testable and what makes a payroll run reproducible.

Geofencing validates punches against branch coordinates using browser geolocation, with a Leaflet-based branch editor for the operations team.

Payroll as versioned rule engines

Base and overtime pay, BPJS contributions, a PPh 21 TER-style band table, THR and leave cash-out each live in their own separately versioned engine. A payslip records which rule version produced it.

Every money value moves through decimal.js fixed-point arithmetic. Floating point has no place in a system that has to agree with a bank file to the rupiah.

Each run passes a three-phase hashed sign-off — HR, then Accounting, then Director — before it is finalized and becomes immutable; then it produces payslips as HTML and React-PDF — and a checksummed bank transfer file, generated by byte-carefully patching the bank's macro-preserving XLSM template, built to remove the re-keying step between payroll and the bank.

Access control that can be demonstrated

A permission catalog (158 codes at the August 2026 cutover, 161 since v10.1.0) is resolved per request as access-level bundle plus per-user grants minus restrictions, then narrowed by scope — all, team, or self — with field-level masking on top.

Restricted fields — national ID, tax ID, bank accounts, social-security numbers — are encrypted at the application layer with AES-256-GCM and versioned ciphertext, so a database dump is not a personal-data breach.

Migration without a leap of faith

The legacy 291-column workbook was not thrown away; it was kept as the reconciliation reference. CLI tooling imports historical payroll, diffs environments, and cross-checks computed results against the workbook baseline.

A spreadsheet-style master-data workspace with staged edits, Excel-style fill and paste, atomic validation and org-chart cycle detection gives HR the ergonomics they already knew, with a full export → edit → import round trip that previews, applies transactionally, and rolls back.

What was hard

Making a payroll run provable

It is not enough to compute the right number. You have to be able to show, months later, which rule version produced it, from which attendance events, and prove nobody edited it afterwards. That requirement drove three decisions at once: the append-only ledger, the versioned engines, and immutability on finalized runs.

Generating a bank file in the bank's own template

The bank's bulk-transfer format is a macro-bearing XLSM template. Regenerating it from scratch breaks the macros; the file has to be patched byte-carefully so the workbook stays valid and checksummed. It is built to remove the hand-keying step that sits between a finalized payroll run and actual money moving.

Statutory rules that nobody will confirm in writing

Tax and contribution rates are documented in the repo as UNVERIFIED pending confirmation with Accounting, with a standing production-gaps register tracking what remains. Encoding uncertainty as a tracked, visible gap rather than a confident constant is the honest engineering choice, and it is how the rule versions earn their keep.

Server actions instead of an API surface

At the August 2026 cutover there were 108 page routes, and every route handler was a read-only GET for a file or an export. Every mutation went through a server action that authorizes, scopes, validates and audits before it touches a module, so there was no second, weaker path into the data, which is the point when the data is salaries and national IDs. The four endpoints that write today are authenticated: a token for the screening integration, the signed-in session for HR reminders.

AI and automation

AI

No AI runs in attendance or payroll, and that is deliberate: nothing probabilistic belongs between an attendance event and a payslip. The only model calls are in recruitment, where screening ranks, scores and recommends, and a person decides. AI was also the delivery method: large releases ran a documented multi-agent workflow of parallel read-only audit agents, a verifying integrator, parallel build lanes, then a fresh set of adversarial reviewers whose blockers had to be cleared before the release was recorded.

Automation

Attendance punches become priced minute buckets automatically; overtime derives from actual attendance with no request step; BPJS, PPh 21 and THR, plus the company's own 78-hour monthly overtime threshold, are computed by versioned engines; and the bank transfer file is generated directly from the finalized run instead of being re-typed into the bank's converter workbook. Operationally, CLI scripts automate legacy import, cross-environment reconciliation, production integrity checks and clean-state verification — and a boot-time hook flags a production database contaminated with demo records.

Direction and delivery

Releases are numbered in the changelog and recorded in a version table inside the app, which the release pipeline writes itself; through v10.0.5 (4 Sep 2026) each numbered release also had a git tag matching its container image. Since September 2026 a release pipeline carries every merged change as one immutable image through staging and smoke tests. Changes to payroll, leave, shared calculations, the database layer, access control or the recruitment AI wait at staging until I promote that exact image, and an integrity compare after every production release rolls it back automatically if anything protected moved. A push that carries a migration releases nothing, so schema changes go in separately, ahead of the code; the four migrations of v10.1.0 were first rehearsed on a restored copy of production before it went live on 3 Oct 2026. The cutover-era release entries state the management request each answers, what was deliberately NOT changed — 'the calculation-owning files and prisma/ are byte-identical to the previous version, verified by diff' — and the gates it passed. A written, permanent non-destructive deployment invariant governs what may never be done to production.

Size signals

Domain modules
17
Prisma models
102
Page routes
108
Permission codes
158
Migrations
29
Numbered releases
48

Counted from the source repository at release v9.30, 27 August 2026. No impact metric is claimed that the source does not prove.

What I would tell the next person

  • Purity is a delivery strategy, not an aesthetic. The rule engines take no I/O and no clock, which is the only reason a month's payroll can be re-run and compared.
  • Write down what you did not change. Release notes that name the untouched, diff-verified files buy more trust from a finance team than any test count.
  • Encode unresolved questions as tracked gaps with owners. A confident constant that nobody verified is worse than a flagged one.

Technologies and access

  • TypeScript
  • Next.js 16
  • React 19
  • PostgreSQL
  • Prisma 6
  • decimal.js
  • Zod 4
  • AES-256-GCM
  • argon2
  • React-PDF
  • exceljs
  • Leaflet
  • Tailwind CSS 4
  • Vitest
  • Playwright
  • Docker
  • GitHub Actions
  • GCP Cloud Run
  • Cloud SQL

Integrations

  • Bulk-transfer file in the bank's own macro-enabled XLSM template
  • Legacy payroll & master-data workbooks
  • Browser Geolocation + Leaflet geofencing
  • GCP Cloud Run / Cloud SQL / Artifact Registry

Internal, login-gated system holding real employee data. No production screens are published.