Customers cannot see each other
Isolation is enforced by row-level security in the database, not by what a screen
chooses to display. Every query, from every part of the application, is filtered
before it returns a row. Tested by querying as an employee of one company across
thirteen tables — employees, payslips, documents, audit logs and the rest — the
number of rows returned belonging to any other company is zero.
Pay is not company gossip
Salaries, payslips and salary history are visible to administrators, to the employee
themselves, and to a manager only for their own direct reports. This is enforced in
the same layer as tenant separation, so it holds no matter which screen or API the
request came through.
Identity data is narrower still
National identifiers and bank details are held in a separate table with its own
policy: administrators and the employee, and nobody else. Managers are deliberately
excluded — running a team is not a reason to hold someone’s national identity
number.
Sensitive fields render masked
Identity and account numbers show as their last four digits and are revealed by a
deliberate click. Each reveal is written to the audit log, because reading a payroll
screen is routine and uncovering a colleague’s identity number is not.
Changes leave evidence
Creates, updates and deletes across people, payroll, leave, documents and settings
are captured at the database level with the previous and new values. A change made
by an import or a script is logged identically to one made by hand, because the
capture is below the application rather than inside it.
Access is joined up
Changing someone’s role updates their permissions and their session token in the
same operation. A role that is right in the database and stale in the token is a
whole category of quiet security bug, and it is closed by construction here.