Structural reference · drawn from source

Avali Atlas

One system, five maps. Each answers a different question about how Avali is put together — pick whichever one matches what you were trying to ask.

Drawn from the uploaded api/ and assets/js/ sources plus the live DEMO and IKO tenants. Where a file wasn't in the upload it is named as inferred rather than shown as fact.

Map one

Runtime layers

A single-page front end talking to flat PHP endpoints over one database. No framework, no build step, no application server — the browser holds all the state and the server holds all the truth.

BROWSER index.html — single-page shell 17 JS modules · app.js carries the bulk all view state lives here employee-portal.js separate surface, same API fetch · JSON APACHE .htaccess — mod_rewrite maps /api/employees → api/employees.php · blocks direct .php access · sets security headers PHP ENDPOINTS · api/ ~20 endpoint files + shared libs employees · periods · advances · reports · import · reconcile … tax.php — the calculation engine requireAuth() on every request SMS gateway · Anthropic API metered by the credits wallet cron.php — nightly loan ledger sweep PDO → MySQL · one shared database
The tenant boundary is a WHERE clause. Every layer above is shared by all companies; isolation lives entirely in company_id being present on each statement — 44 of 45 by the team's own audit. That is what ADR-001 proposes to move into the connection itself.
Worth knowing

There is no $_SESSION anywhere. Auth is a token signed with APP_SECRET, held in the browser and re-verified per request by requireAuth(). That makes the PHP layer stateless — which is why it could scale horizontally, and why containerising it is even plausible.

Map two

Product surface

What a user can actually open, and how it nests. Three sidebar groups, a Payroll Composition tab strip, and an HR hub with its own five-group rail — plus a second front door for employees.

SIDEBAR Muster Roll Dashboard Pay Entry Muster Roll Payroll Composition → Contractors People Employees HR hub → Approvals Reports Reports P9 Cards Payslips Recon Payroll Composition · the registers Salaries & Allowances Commissions Per Diem Advances Co-op / Sacco HR hub · five task groups Time & attendance — leave, attendance, overtime, absenteeism Employee lifecycle — onboarding, contracts, offboarding Conduct & safety — disciplinary, WIBA Engagement — assets, rewards, birthdays, performance Communications — announcements Plugin layer attendance · WIBA · assets · expenses · birthdays register a tab into a section; installed globally, enabled per company SECOND FRONT DOOR Employee self-service portal payslips · P9 card · contracts (view, e-sign, decline) · advances · documents · assets · leave Separate module, same API, employee-scoped token. Gated on a portal login being active — the dashboard's "can't sign in" count. Cross-company (superadmin) company switcher · users · audit log · billing and credits · tax rate settings · migrations The only surface that reads across tenants. Every surface is scoped twice — by company, and by pay period
Two coordinates, always. Nothing in Avali means anything without a company and a period. Most of the defensive code in the codebase — the settle-window check, the period banner, the historical-period pill — exists to stop an action landing on the wrong one.

Map three · the important one

Path of the money

If you only look at one map, this is it. Everything distinctive about Avali — the reconciliation tooling, the period-health checks, the push-and-recall vocabulary — follows from a single structural fact visible here.

REGISTERS — each entry moves pending → pushed → recalled / cancelled Allowances & non-cash Overtime (1.5× / 2.0×) Absenteeism Commissions Per diem (trips + days) Advances & loans Co-op / Sacco company_id · employee_id · pay_period PUSH gate recall reverses it payroll_records the muster roll — one row per employee per month, plus two JSON columns for the detail Pay Entry edits the same columns by hand — a second writable surface writes directly tax.php gross − NSSF, SHIF, AHL = taxable income → PAYE bands − relief, HELB = net pay Outputs payslips · P9 cards bank transfer file PAYE · SHIF · NSSF AHL · NITA schedules BECAUSE THERE ARE TWO WRITERS, THREE MECHANISMS ASSERT THEY AGREE Period health Seven indexed checks per period. Flags critical when payroll holds more overtime, absence or commission than its own register issued. Commissions Reconcile Compares pushed register totals against the muster column, per employee, every period. Built after a real client incident that ran undetected for months. reconcile.php Upload the month's external workbook; compare it line by line to the system. Read-only, writes nothing. Treats the outside file as the challenger. read the muster + the registers
The muster roll has two writers. Registers push into payroll_records, and Pay Entry edits the same columns by hand. Nothing in the schema stops them disagreeing, so the disagreement is detected instead of prevented — which is why Avali carries three independent reconciliation mechanisms that most payroll systems don't have. Read that as a scar, not an ornament: each one exists because a real client's numbers drifted.
The same shape, repeatedly

Almost every defect found across this codebase is a variant of one pattern: a rule enforced on the read path but not the write path. A checklist item that can't be hand-ticked in the UI but can be through the endpoint. A contract that can't be signed as a draft in the portal but could be by id. A client figure that's ignored for overtime and absence, but trusted for per diem. The registers-versus-muster split is the original of that pattern, and the reconciliation tooling is what containing it looks like.

Map four

The month's lifecycle

A pay period is a small state machine with real teeth. Each transition has a different role behind it, and locking is the one that converts provisional figures into facts.

DRAFT figures editable SUBMITTED awaiting approval APPROVED not yet paid LOCKED paid · immutable HR admin admin reject → back to draft (admin) re-open (admin) unlock — superadmin only approval is refused while register money sits approved-but-unpushed LOCKING blocks every edit · releases payslips and statutory schedules · finalises advance recoveries
Locking is the commit. Until it happens, advance recoveries exist only as a schedule — the deductions appear on payslips while the register still shows them provisional. A past month left unlocked is money charged on paper but never collected, which is exactly what the period-health sweep's "unlocked charges" warning is looking for.

Map five

Data model core

Roughly thirty tables, but only a handful are load-bearing. What matters structurally is which of them carry company_id — because that column is currently the only thing holding one client's payroll apart from another's.

SHARED REGISTRY companies tax_settings JSON users identity user_companies the grant employees company_id · payroll_no · basic pay reports_to · apply_paye / nssf / shif id_number + kra_pin encrypted payroll_records company_id + employee_id + period every money column, plus allowances_json / deductions_json payroll_periods company_id · status draft → locked Register tables — seven, one shape allowance_entries · overtime_records absenteeism_records · commission_entries perdiem · advances · co-op memberships all: company_id · employee_id · period · status Everything else, also company-scoped leave · attendance · contracts + versions · onboarding · disciplinary · WIBA · documents · assets · audit_log The tenant boundary is this column, and nothing else one database · one connection · every company inside it
An audit result, not a schema guarantee. The difference matters: a WHERE clause can be forgotten in a new query and nothing will fail loudly. Moving the boundary into the connection — one database per tenant, resolved before any query runs — is what turns a convention that has to be maintained into a structure that holds by itself.
The five maps, and what each one is for
MapUse it whenIts single claim
Runtime layersTalking about hosting, scaling, or deploymentStateless PHP over one shared database
Product surfaceOnboarding someone, or scoping a feature's homeEverything is scoped by company and period
Path of the moneyDebugging a wrong figure, or explaining the productThe muster roll has two writers
Month's lifecycleAnything touching approval, payment or immutabilityLocking is the commit
Data model coreResidency, tenancy, deletion, exportThe tenant boundary is one column