KPI Media · Internal · Engineering documentation

The People Platform, end to end

Eight pages. What the system is for and what it must do, what has been decided about how to build it and what was rejected on the way, how it is built, how it is set up once, how it is operated — and the two questionnaires that are out with Finance and HR. Written to be read before the technical conversation with the engineer, not after it.

32 people 4 modules 3 roles 40 decision records 13 migrations 300 tests 2 questionnaires out

What this page is, and what it is not

The repository holds the real documents — CONTEXT.md for the language, docs/adr/ for forty decisions with their reasoning, and three engineering manuals. Those are written for the person building or operating the thing, and they say what to do. They do not say what else was on the table.

This page adds that. The Decisions page is the one that does not exist in the repository: seventeen engineering choices, each with what was rejected, what the choice costs, and whether it is actually settled. That is the page to argue from.

Everything here is a snapshot of 14 August 2026. Where a page here and an ADR disagree, the ADR wins and this page is out of date. Concept is the exception: it is kept exactly as approved on 31 July and is deliberately not corrected, because it is the record of what was signed off rather than a description of the present.

Where it standsThe database layer is finished; nothing is deployed

On 6 August this page said the design was far ahead of the code and nothing had ever been executed. That has flipped. Every database and authorization module is now built, run and proven — thirteen migrations, five schemas, 300 tests against a real Postgres. What has not happened is anything above the database, and anything outside this machine.

Built, run and proven

  • core and ops — employees, RBAC, entities, error log
  • hrms — claims, categories, statutory rates
  • Storage — the receipts bucket, policies over real uploads
  • org — share links, the one anonymous read path
  • leave — policy, types, entitlements, awards, requests
  • The audit trail — triggers on seven tables
  • Six signed matrices, each written before its migration

Waiting on somebody else

  • Payroll — blocked on the fixture table, which is out with Finance as a 20-question, 30-example document
  • Leave settlement at exit — three questions out with HR and Anwar
  • The Indonesian entity’s registered name and number
  • 2026 opening balances, attested (ADR-0022)

Not started

  • Nothing is deployed. All thirteen migrations are local only; ADR-0038 wants three environments and a manual promotion
  • src/ — no application code at all. No shell, no UI, no Node service
  • .github/ — no CI. Deliberately deferred to just before the handover
  • The identity-destruction job (ADR-0025) and the year-end leave close (ADR-0017)

The ordering held, and it earned its cost

Two rules say a document must exist before the code it governs: the RLS matrix before any policy (ADR-0003, amended) and the payroll fixture table before any payroll code (ADR-0039). Both exist for the same reason — tests derived from an implementation only prove the implementation does what it does.

The matrix rule held for all six modules, at a cost of two pull requests each. It also caught four things that would otherwise have been written down as true: a revoke on the storage tables that reports success and changes nothing; a delete-protect trigger nobody knew existed; that anon is stopped by schema access rather than by the function grant everyone was looking at; and that an append-only trigger never fires for a signed-in user, because a missing grant refuses first. None of the four would have been visible in a diff.

The fixture table is still the longest lead item on the project, and it is now waiting on Finance rather than on us.

What is openThree blocking, three to decide, six to check

The shape of this list has changed. On 6 August most of it was work we owed; now most of it is answers we are waiting for, and one thing nobody has started.

Blocking

  • DataThe Indonesian entity’s registered name and registration number. Not in any document we hold. All 32 tracker rows say “KPI Media Pte Ltd” while 22 people are marked BPJS, which requires an Indonesian employer. These appear on statutory filings, so the importer must not run against production until they are real. Unchanged since 6 August.
  • FixturesThe payroll fixture table — now out with Finance. Twenty questions and thirty worked examples, inputs filled in, outputs blank. Payroll implementation is deliberately blocked on it (ADR-0039). It takes calendar time rather than engineering time, and the clock is running with somebody else.
  • DeployNothing has ever been deployed. The Supabase stack is up on Zeabur and its image versions are pinned to match local — but our thirteen migrations have never run anywhere except this machine. ADR-0038 wants three environments and a manual promotion, and none of that exists.

Three things only you can decide

  • WorkflowPR review turnaround. technical-design.md §13 says the senior reviews every PR; it does not say how fast. A full-time engineer blocked on review cannot merge, cannot start the next ticket, and will either idle or guess. Commit to a number — same working day is the usual one — and name where questions go between reviews.
  • PolicyThe payroll approver pool is five people, not the four ADR-0020 names. Computed from permissions rather than from the ADR’s prose it includes the engineer, because super_admin holds every permission in the catalogue. ADR-0020’s overlap rule would therefore constrain his leave alongside Finance’s and the CEO’s. Three options are written up; the recommendation is a marker permission granted deliberately rather than inherited.
  • AccessMay a person read the audit rows about themselves? Currently no. The PDPA gives an individual a right of access to their own personal data, and “who opened your passport scan” is that — the same argument that reversed an earlier decision about reading your own compensation. It is one policy line and one test either way, and it is flagged rather than settled quietly.

Resolved since 6 August: the missing package.json (the suite runs, 300 tests), and Q1 and Q3 of the core matrix, both ratified unchanged at signature.

Facts to verify before Zeabur

Not decisions. But each one changes the plan if it comes back wrong, and all of them are cheap to check. One has been answered since 6 August.

CheckWhat it changes if the answer is no
Does the Supabase template deploy twice into one project?The entire environment topology assumes it. Name and port collisions are the likely failure. Falling back means two projects and the dashboard argument returns.
Does Zeabur offer a disk alarm?setup.md specifies 70%. If the feature does not exist, the shared-disk mitigation is imaginary and has to become a job of ours posting to Slack.
What image versions does the template pin? answeredRecorded at the head of config.toml and matched locally — Postgres 15.8.1.085, PostgREST v13.0.7, storage-api v1.28.2, and eight more. One is still unpinned: minio/minio:latest, which holds the backups. That part is a live risk rather than a resolved item.
Does the Node image carry pg_dump?ADR-0037’s rejection of Cloudflare Workers rests on it, and postgresql-client is absent from most Node base images. No pg_dump, no backups.
Which domain hosts Google Workspace — kpimedia.sg or kpimedia.co?Staff addresses are @kpimedia.sg, but setup.md §5 says to merge Resend’s SPF into Workspace’s existing record on kpimedia.co. One of those is wrong. A second v=spf1 breaks mail on whichever domain it lands on, and the answer also decides what payslips are sent from.
Is the senior a Workspace admin?The OAuth consent screen must be Internal. That is the real domain restriction — not a GoTrue setting.
Who holds GoDaddy DNS and Cloudflare billing?Three CNAMEs and the Resend records go into GoDaddy. Not having that access on the day stops setup dead.

Two inconsistencies found while assembling this page

Legal entity count. The Concept screen’s masthead says three legal entities. Migration 0005 creates two — KPI_SG and KPI_ID. One of them is wrong, and since statutory scheme derives from the legal entity (ADR-0023), it is not cosmetic.

A placeholder that does not look like one. 0005 leaves both registration numbers null on the stated principle that a null is visibly missing while a plausible-looking value reads as filled in. But the same row carries 'PT KPI Media Indonesia' as the registered name — which is a guess, held to a lower standard than the number beside it. Either it is confirmed or it should be null too.

Where to goThe other seven pages