KPI Media · Internal · Engineering documentation

The People Platform, end to end

Six 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, and how it is operated. Written to be read before the technical conversation with the engineer, not after it.

32 people 4 modules 3 roles 40 decision records 16 engineering choices 3 blocking 7 facts to verify

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: sixteen 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 6 August 2026. Where a page here and an ADR disagree, the ADR wins and this page is out of date.

Where it standsWritten, unrun, and missing

The design is far ahead of the code. Nothing in the database layer has ever been executed — Docker was not running when it was written, and app/ still has no package.json, so none of it can run yet.

Settled and written

  • Forty ADRs, three grilling sessions
  • CONTEXT.md — the domain language
  • Technical design, setup, runbook
  • Migrations 0001 schema, 0002 RBAC
  • Three roles, permission catalogue

Written but never run

  • 0003 legal entity, account-link trigger
  • 0004_rls.sql — every policy
  • 0005 departments and entities
  • seed.sql — 32 real people
  • tests/rls/ — matrix and suite
  • scripts/import-employees.ts

Does not exist

  • app/package.json — no scaffold, so nothing above can execute
  • .github/ — no workflows, so the checks branch protection requires have nothing to run
  • RLS matrices for hrms, org, leave, Storage
  • The golden payroll fixture table

The ordering that matters

Two rules in the ADRs 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 core matrix was written first and held the line. The fixture table has not been started, and it needs Finance and a calendar, so it is the longest lead item on the project.

What is openThree blocking, two undecided, seven to check

Nothing here blocks reading the rest of these pages. Two of the three blocking items block the first real work.

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.
  • ScaffoldNo package.json. Until it exists, the RLS suite cannot run and the RLS policies are unproven prose. First ticket.
  • FixturesThe payroll fixture table. Twenty to thirty worked examples, authored with Finance. Payroll implementation is deliberately blocked on it (ADR-0039), and it takes calendar time rather than engineering time, so it should start now.

Two 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.
  • PolicyQ1 and Q3 of the RLS matrix — may a person edit their own directory row, and may finance_hr read core.account. Both were taken as recommendations rather than ruled on. They are written into 0004_rls.sql either way, so a different answer means a migration.

Facts to verify before Zeabur

Not decisions. But each one changes the plan if it comes back wrong, and all seven are cheap to check.

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?config.toml must match and cannot be written until this is known. Version drift between local, dev and production is the failure this setup exists to prevent.
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 Workspacekpimedia.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 twoKPI_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 five pages